├── hugo-task ├── .gitignore ├── icon.png ├── ps_modules │ └── VstsTaskSdk │ │ ├── Minimatch.dll │ │ ├── VstsTaskSdk.dll │ │ ├── VstsTaskSdk.psd1 │ │ ├── Strings │ │ └── resources.resjson │ │ │ ├── zh-CN │ │ │ └── resources.resjson │ │ │ ├── zh-TW │ │ │ └── resources.resjson │ │ │ ├── ko-KR │ │ │ └── resources.resjson │ │ │ ├── ja-jp │ │ │ └── resources.resjson │ │ │ ├── ru-RU │ │ │ └── resources.resjson │ │ │ ├── en-US │ │ │ └── resources.resjson │ │ │ ├── fr-fr │ │ │ └── resources.resjson │ │ │ ├── it-IT │ │ │ └── resources.resjson │ │ │ ├── es-es │ │ │ └── resources.resjson │ │ │ └── de-de │ │ │ └── resources.resjson │ │ ├── lib.json │ │ ├── OutFunctions.ps1 │ │ ├── ToolFunctions.ps1 │ │ ├── TraceFunctions.ps1 │ │ ├── LocalizationFunctions.ps1 │ │ ├── VstsTaskSdk.psm1 │ │ ├── LongPathFunctions.ps1 │ │ ├── LegacyFindFunctions.ps1 │ │ ├── InputFunctions.ps1 │ │ ├── LoggingCommandFunctions.ps1 │ │ ├── ServerOMFunctions.ps1 │ │ └── FindFunctions.ps1 ├── Get-HugoExecutable.ps1 ├── Invoke-Hugo.ps1 ├── HugoTask.ps1 ├── Select-HugoVersion.ps1 └── task.json ├── .gitignore ├── hugo-taskV2 ├── .gitignore ├── icon.png ├── test │ ├── SITE │ │ ├── config.toml │ │ └── archetypes │ │ │ └── default.md │ ├── testDownload.ps1 │ └── testRun.ps1 ├── .eslintignore ├── .eslintrc.js ├── tsconfig.json ├── package.json ├── task.json └── src │ └── HugoTask.ts ├── images ├── hugo-logo.png ├── BuildTaskArguments.png ├── hugo-vsts-ext-build-01.png ├── hugo-vsts-ext-build-02.png ├── hugo-vsts-ext-build-03.png ├── hugo-vsts-ext-build-04.png ├── hugo-vsts-ext-build-05.png ├── hugo-vsts-ext-build-06.png ├── hugo-vsts-ext-build-07.png ├── hugo-vsts-ext-build-08.png ├── hugo-vsts-ext-build-09.png ├── hugo-vsts-ext-build-10.png ├── hugo-vsts-ext-build-11.png ├── hugo-vsts-ext-build-12.png ├── hugo-vsts-ext-build-13.png ├── hugo-vsts-ext-build-14.png ├── hugo-vsts-ext-build-15.png └── hugo-vsts-ext-build-16.png ├── img ├── hugo-128x128.png └── hugo-32x32.png ├── LICENSE.txt ├── README.md ├── vss-extension.json └── hugo-extension-step-by-step.md /hugo-task/.gitignore: -------------------------------------------------------------------------------- 1 | ps_modules/ -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.vsix 2 | PRIVATE/* 3 | -------------------------------------------------------------------------------- /hugo-taskV2/.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | test/PUBLISH 3 | test/TOOLS/ 4 | test/TEMP/ 5 | node_modules/ 6 | -------------------------------------------------------------------------------- /hugo-task/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giuliov/hugo-vsts-extension/HEAD/hugo-task/icon.png -------------------------------------------------------------------------------- /hugo-taskV2/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giuliov/hugo-vsts-extension/HEAD/hugo-taskV2/icon.png -------------------------------------------------------------------------------- /images/hugo-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giuliov/hugo-vsts-extension/HEAD/images/hugo-logo.png -------------------------------------------------------------------------------- /img/hugo-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giuliov/hugo-vsts-extension/HEAD/img/hugo-128x128.png -------------------------------------------------------------------------------- /img/hugo-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giuliov/hugo-vsts-extension/HEAD/img/hugo-32x32.png -------------------------------------------------------------------------------- /images/BuildTaskArguments.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giuliov/hugo-vsts-extension/HEAD/images/BuildTaskArguments.png -------------------------------------------------------------------------------- /hugo-taskV2/test/SITE/config.toml: -------------------------------------------------------------------------------- 1 | baseURL = 'http://example.org/' 2 | languageCode = 'en-us' 3 | title = 'My New Hugo Site' 4 | -------------------------------------------------------------------------------- /images/hugo-vsts-ext-build-01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giuliov/hugo-vsts-extension/HEAD/images/hugo-vsts-ext-build-01.png -------------------------------------------------------------------------------- /images/hugo-vsts-ext-build-02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giuliov/hugo-vsts-extension/HEAD/images/hugo-vsts-ext-build-02.png -------------------------------------------------------------------------------- /images/hugo-vsts-ext-build-03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giuliov/hugo-vsts-extension/HEAD/images/hugo-vsts-ext-build-03.png -------------------------------------------------------------------------------- /images/hugo-vsts-ext-build-04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giuliov/hugo-vsts-extension/HEAD/images/hugo-vsts-ext-build-04.png -------------------------------------------------------------------------------- /images/hugo-vsts-ext-build-05.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giuliov/hugo-vsts-extension/HEAD/images/hugo-vsts-ext-build-05.png -------------------------------------------------------------------------------- /images/hugo-vsts-ext-build-06.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giuliov/hugo-vsts-extension/HEAD/images/hugo-vsts-ext-build-06.png -------------------------------------------------------------------------------- /images/hugo-vsts-ext-build-07.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giuliov/hugo-vsts-extension/HEAD/images/hugo-vsts-ext-build-07.png -------------------------------------------------------------------------------- /images/hugo-vsts-ext-build-08.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giuliov/hugo-vsts-extension/HEAD/images/hugo-vsts-ext-build-08.png -------------------------------------------------------------------------------- /images/hugo-vsts-ext-build-09.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giuliov/hugo-vsts-extension/HEAD/images/hugo-vsts-ext-build-09.png -------------------------------------------------------------------------------- /images/hugo-vsts-ext-build-10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giuliov/hugo-vsts-extension/HEAD/images/hugo-vsts-ext-build-10.png -------------------------------------------------------------------------------- /images/hugo-vsts-ext-build-11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giuliov/hugo-vsts-extension/HEAD/images/hugo-vsts-ext-build-11.png -------------------------------------------------------------------------------- /images/hugo-vsts-ext-build-12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giuliov/hugo-vsts-extension/HEAD/images/hugo-vsts-ext-build-12.png -------------------------------------------------------------------------------- /images/hugo-vsts-ext-build-13.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giuliov/hugo-vsts-extension/HEAD/images/hugo-vsts-ext-build-13.png -------------------------------------------------------------------------------- /images/hugo-vsts-ext-build-14.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giuliov/hugo-vsts-extension/HEAD/images/hugo-vsts-ext-build-14.png -------------------------------------------------------------------------------- /images/hugo-vsts-ext-build-15.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giuliov/hugo-vsts-extension/HEAD/images/hugo-vsts-ext-build-15.png -------------------------------------------------------------------------------- /images/hugo-vsts-ext-build-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giuliov/hugo-vsts-extension/HEAD/images/hugo-vsts-ext-build-16.png -------------------------------------------------------------------------------- /hugo-taskV2/test/SITE/archetypes/default.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "{{ replace .Name "-" " " | title }}" 3 | date: {{ .Date }} 4 | draft: true 5 | --- 6 | 7 | -------------------------------------------------------------------------------- /hugo-task/ps_modules/VstsTaskSdk/Minimatch.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giuliov/hugo-vsts-extension/HEAD/hugo-task/ps_modules/VstsTaskSdk/Minimatch.dll -------------------------------------------------------------------------------- /hugo-task/ps_modules/VstsTaskSdk/VstsTaskSdk.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giuliov/hugo-vsts-extension/HEAD/hugo-task/ps_modules/VstsTaskSdk/VstsTaskSdk.dll -------------------------------------------------------------------------------- /hugo-task/ps_modules/VstsTaskSdk/VstsTaskSdk.psd1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giuliov/hugo-vsts-extension/HEAD/hugo-task/ps_modules/VstsTaskSdk/VstsTaskSdk.psd1 -------------------------------------------------------------------------------- /hugo-taskV2/.eslintignore: -------------------------------------------------------------------------------- 1 | # don't ever lint node_modules 2 | node_modules 3 | # don't lint build output (make sure it's set to your correct build folder name) 4 | dist -------------------------------------------------------------------------------- /hugo-taskV2/.eslintrc.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | module.exports = { 3 | root: true, 4 | parser: '@typescript-eslint/parser', 5 | plugins: [ 6 | '@typescript-eslint', 7 | ], 8 | extends: [ 9 | 'eslint:recommended', 10 | 'plugin:@typescript-eslint/recommended', 11 | ], 12 | }; -------------------------------------------------------------------------------- /hugo-taskV2/test/testDownload.ps1: -------------------------------------------------------------------------------- 1 | $env:INPUT_HUGOVERSION = "0.68.1" 2 | $env:INPUT_HUGOVERSION = "latest" 3 | $env:INPUT_EXTENDEDVERSION = 'true' 4 | 5 | $env:TASK_TEST_TRACE=1 6 | $env:Agent_ToolsDirectory='C:\src\github.com\giuliov\hugo-azdo-extension\hugo-task\test\TOOLS' 7 | $env:Agent_TempDirectory='C:\src\github.com\giuliov\hugo-azdo-extension\hugo-task\test\TEMP' 8 | $env:Agent_Version='2.115.0' 9 | node ..\dist\src\HugoTask.js 10 | -------------------------------------------------------------------------------- /hugo-taskV2/test/testRun.ps1: -------------------------------------------------------------------------------- 1 | $env:INPUT_SOURCE = '.\test\SITE' 2 | $env:INPUT_DESTINATION = '..\PUBLISH' 3 | $env:INPUT_BASEURL = $null 4 | $env:INPUT_BUILDDRAFTS = 'false' 5 | $env:INPUT_BUILDEXPIRED = 'false' 6 | $env:INPUT_BUILDFUTURE = 'false' 7 | $env:INPUT_ADDITIONALARGS = $null 8 | 9 | $env:TASK_TEST_TRACE=1 10 | $env:Agent_ToolsDirectory='C:\src\github.com\giuliov\hugo-azdo-extension\hugo-task\test\TOOLS' 11 | $env:Agent_TempDirectory='C:\src\github.com\giuliov\hugo-azdo-extension\hugo-task\test\TEMP' 12 | $env:Agent_Version='2.115.0' 13 | node ..\dist\src\HugoTask.js -------------------------------------------------------------------------------- /hugo-taskV2/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES6", 4 | "module": "commonjs", 5 | "watch": false, 6 | "outDir": ".", 7 | "moduleResolution": "node", 8 | "rootDirs": [ 9 | "src/", 10 | "test/" 11 | ], 12 | "outDir": "dist/src", 13 | "sourceMap": true 14 | }, 15 | "baseUrl": ".", 16 | "paths": { 17 | "*": [ 18 | "node_modules/*", 19 | "src/types/*" 20 | ] 21 | }, 22 | "exclude": [ 23 | "node_modules" 24 | ] 25 | } 26 | 27 | -------------------------------------------------------------------------------- /hugo-task/ps_modules/VstsTaskSdk/Strings/resources.resjson/zh-CN/resources.resjson: -------------------------------------------------------------------------------- 1 | { 2 | "loc.messages.PSLIB_ContainerPathNotFound0": "找不到容器路径:“{0}”", 3 | "loc.messages.PSLIB_EndpointAuth0": "“{0}”服务终结点凭据", 4 | "loc.messages.PSLIB_EndpointUrl0": "“{0}”服务终结点 URL", 5 | "loc.messages.PSLIB_EnumeratingSubdirectoriesFailedForPath0": "枚举路径的子目录失败:“{0}”", 6 | "loc.messages.PSLIB_FileNotFound0": "找不到文件: {0}。", 7 | "loc.messages.PSLIB_Input0": "“{0}”输入", 8 | "loc.messages.PSLIB_InvalidPattern0": "无效的模式:“{0}”", 9 | "loc.messages.PSLIB_LeafPathNotFound0": "找不到叶路径:“{0}”", 10 | "loc.messages.PSLIB_PathLengthNotReturnedFor0": "路径规范化/扩展失败。路径长度不是由“{0}”的 Kernel32 子系统返回的", 11 | "loc.messages.PSLIB_PathNotFound0": "找不到路径:“{0}”", 12 | "loc.messages.PSLIB_Process0ExitedWithCode1": "过程“{0}”已退出,代码为“{1}”。", 13 | "loc.messages.PSLIB_Required0": "必需: {0}", 14 | "loc.messages.PSLIB_StringFormatFailed": "字符串格式无效。", 15 | "loc.messages.PSLIB_StringResourceKeyNotFound0": "找不到字符串资源关键字:“{0}”", 16 | "loc.messages.PSLIB_TaskVariable0": "“{0}”任务变量" 17 | } -------------------------------------------------------------------------------- /hugo-task/ps_modules/VstsTaskSdk/Strings/resources.resjson/zh-TW/resources.resjson: -------------------------------------------------------------------------------- 1 | { 2 | "loc.messages.PSLIB_ContainerPathNotFound0": "找不到容器路徑: '{0}'", 3 | "loc.messages.PSLIB_EndpointAuth0": "'{0}' 服務端點認證", 4 | "loc.messages.PSLIB_EndpointUrl0": "'{0}' 服務端點 URL", 5 | "loc.messages.PSLIB_EnumeratingSubdirectoriesFailedForPath0": "為路徑列舉子目錄失敗: '{0}'", 6 | "loc.messages.PSLIB_FileNotFound0": "找不到檔案: '{0}'", 7 | "loc.messages.PSLIB_Input0": "'{0}' 輸入", 8 | "loc.messages.PSLIB_InvalidPattern0": "模式無效: '{0}'", 9 | "loc.messages.PSLIB_LeafPathNotFound0": "找不到分葉路徑: '{0}'", 10 | "loc.messages.PSLIB_PathLengthNotReturnedFor0": "路徑正規化/展開失敗。Kernel32 子系統未傳回 '{0}' 的路徑長度", 11 | "loc.messages.PSLIB_PathNotFound0": "找不到路徑: '{0}'", 12 | "loc.messages.PSLIB_Process0ExitedWithCode1": "處理序 '{0}' 以返回碼 '{1}' 結束。", 13 | "loc.messages.PSLIB_Required0": "必要項: {0}", 14 | "loc.messages.PSLIB_StringFormatFailed": "字串格式失敗。", 15 | "loc.messages.PSLIB_StringResourceKeyNotFound0": "找不到字串資源索引鍵: '{0}'", 16 | "loc.messages.PSLIB_TaskVariable0": "'{0}' 工作變數" 17 | } -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2017 Giulio Vian 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. -------------------------------------------------------------------------------- /hugo-task/ps_modules/VstsTaskSdk/Strings/resources.resjson/ko-KR/resources.resjson: -------------------------------------------------------------------------------- 1 | { 2 | "loc.messages.PSLIB_ContainerPathNotFound0": "컨테이너 경로를 찾을 수 없음: '{0}'", 3 | "loc.messages.PSLIB_EndpointAuth0": "'{0}' 서비스 끝점 자격 증명", 4 | "loc.messages.PSLIB_EndpointUrl0": "'{0}' 서비스 끝점 URL", 5 | "loc.messages.PSLIB_EnumeratingSubdirectoriesFailedForPath0": "경로에 대해 하위 디렉터리를 열거하지 못함: '{0}'", 6 | "loc.messages.PSLIB_FileNotFound0": "{0} 파일을 찾을 수 없습니다.", 7 | "loc.messages.PSLIB_Input0": "'{0}' 입력", 8 | "loc.messages.PSLIB_InvalidPattern0": "잘못된 패턴: '{0}'", 9 | "loc.messages.PSLIB_LeafPathNotFound0": "Leaf 경로를 찾을 수 없음: '{0}'", 10 | "loc.messages.PSLIB_PathLengthNotReturnedFor0": "경로 정규화/확장에 실패했습니다. 다음에 대해 Kernel32 subsystem에서 경로 길이를 반환하지 않음: '{0}'", 11 | "loc.messages.PSLIB_PathNotFound0": "경로를 찾을 수 없음: '{0}'", 12 | "loc.messages.PSLIB_Process0ExitedWithCode1": "'{1}' 코드로 '{0}' 프로세스가 종료되었습니다.", 13 | "loc.messages.PSLIB_Required0": "필수: {0}", 14 | "loc.messages.PSLIB_StringFormatFailed": "문자열을 포맷하지 못했습니다.", 15 | "loc.messages.PSLIB_StringResourceKeyNotFound0": "문자열 리소스 키를 찾을 수 없음: '{0}'", 16 | "loc.messages.PSLIB_TaskVariable0": "'{0}' 작업 변수" 17 | } -------------------------------------------------------------------------------- /hugo-task/ps_modules/VstsTaskSdk/Strings/resources.resjson/ja-jp/resources.resjson: -------------------------------------------------------------------------------- 1 | { 2 | "loc.messages.PSLIB_ContainerPathNotFound0": "コンテナーのパスが見つかりません: '{0}'", 3 | "loc.messages.PSLIB_EndpointAuth0": "'{0}' サービス エンドポイントの資格情報", 4 | "loc.messages.PSLIB_EndpointUrl0": "'{0}' サービス エンドポイントの URL", 5 | "loc.messages.PSLIB_EnumeratingSubdirectoriesFailedForPath0": "パス '{0}' のサブディレクトリを列挙できませんでした", 6 | "loc.messages.PSLIB_FileNotFound0": "ファイルが見つかりません: '{0}'", 7 | "loc.messages.PSLIB_Input0": "'{0}' 入力", 8 | "loc.messages.PSLIB_InvalidPattern0": "使用できないパターンです: '{0}'", 9 | "loc.messages.PSLIB_LeafPathNotFound0": "リーフ パスが見つかりません: '{0}'", 10 | "loc.messages.PSLIB_PathLengthNotReturnedFor0": "パスの正規化/展開に失敗しました。Kernel32 サブシステムからパス '{0}' の長さが返されませんでした", 11 | "loc.messages.PSLIB_PathNotFound0": "パスが見つかりません: '{0}'", 12 | "loc.messages.PSLIB_Process0ExitedWithCode1": "プロセス '{0}' がコード '{1}' で終了しました。", 13 | "loc.messages.PSLIB_Required0": "必要: {0}", 14 | "loc.messages.PSLIB_StringFormatFailed": "文字列のフォーマットに失敗しました。", 15 | "loc.messages.PSLIB_StringResourceKeyNotFound0": "文字列のリソース キーが見つかりません: '{0}'", 16 | "loc.messages.PSLIB_TaskVariable0": "'{0}' タスク変数" 17 | } -------------------------------------------------------------------------------- /hugo-task/ps_modules/VstsTaskSdk/lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "messages": { 3 | "PSLIB_AgentVersion0Required": "Agent version {0} or higher is required.", 4 | "PSLIB_ContainerPathNotFound0": "Container path not found: '{0}'", 5 | "PSLIB_EndpointAuth0": "'{0}' service endpoint credentials", 6 | "PSLIB_EndpointUrl0": "'{0}' service endpoint URL", 7 | "PSLIB_EnumeratingSubdirectoriesFailedForPath0": "Enumerating subdirectories failed for path: '{0}'", 8 | "PSLIB_FileNotFound0": "File not found: '{0}'", 9 | "PSLIB_Input0": "'{0}' input", 10 | "PSLIB_InvalidPattern0": "Invalid pattern: '{0}'", 11 | "PSLIB_LeafPathNotFound0": "Leaf path not found: '{0}'", 12 | "PSLIB_PathLengthNotReturnedFor0": "Path normalization/expansion failed. The path length was not returned by the Kernel32 subsystem for: '{0}'", 13 | "PSLIB_PathNotFound0": "Path not found: '{0}'", 14 | "PSLIB_Process0ExitedWithCode1": "Process '{0}' exited with code '{1}'.", 15 | "PSLIB_Required0": "Required: {0}", 16 | "PSLIB_StringFormatFailed": "String format failed.", 17 | "PSLIB_StringResourceKeyNotFound0": "String resource key not found: '{0}'", 18 | "PSLIB_TaskVariable0": "'{0}' task variable" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /hugo-task/ps_modules/VstsTaskSdk/Strings/resources.resjson/ru-RU/resources.resjson: -------------------------------------------------------------------------------- 1 | { 2 | "loc.messages.PSLIB_ContainerPathNotFound0": "Путь к контейнеру не найден: \"{0}\".", 3 | "loc.messages.PSLIB_EndpointAuth0": "Учетные данные конечной точки службы \"{0}\"", 4 | "loc.messages.PSLIB_EndpointUrl0": "URL-адрес конечной точки службы \"{0}\"", 5 | "loc.messages.PSLIB_EnumeratingSubdirectoriesFailedForPath0": "Сбой перечисления подкаталогов для пути: \"{0}\".", 6 | "loc.messages.PSLIB_FileNotFound0": "Файл не найден: \"{0}\".", 7 | "loc.messages.PSLIB_Input0": "Входные данные \"{0}\".", 8 | "loc.messages.PSLIB_InvalidPattern0": "Недопустимый шаблон: \"{0}\".", 9 | "loc.messages.PSLIB_LeafPathNotFound0": "Путь к конечному объекту не найден: \"{0}\".", 10 | "loc.messages.PSLIB_PathLengthNotReturnedFor0": "Сбой нормализации и расширения пути. Длина пути не была возвращена подсистемой Kernel32 для: \"{0}\".", 11 | "loc.messages.PSLIB_PathNotFound0": "Путь не найден: \"{0}\".", 12 | "loc.messages.PSLIB_Process0ExitedWithCode1": "Процесс \"{0}\" завершил работу с кодом \"{1}\".", 13 | "loc.messages.PSLIB_Required0": "Требуется: {0}", 14 | "loc.messages.PSLIB_StringFormatFailed": "Сбой формата строки.", 15 | "loc.messages.PSLIB_StringResourceKeyNotFound0": "Ключ ресурса строки не найден: \"{0}\".", 16 | "loc.messages.PSLIB_TaskVariable0": "Переменная задачи \"{0}\"" 17 | } -------------------------------------------------------------------------------- /hugo-task/ps_modules/VstsTaskSdk/Strings/resources.resjson/en-US/resources.resjson: -------------------------------------------------------------------------------- 1 | { 2 | "loc.messages.PSLIB_AgentVersion0Required": "Agent version {0} or higher is required.", 3 | "loc.messages.PSLIB_ContainerPathNotFound0": "Container path not found: '{0}'", 4 | "loc.messages.PSLIB_EndpointAuth0": "'{0}' service endpoint credentials", 5 | "loc.messages.PSLIB_EndpointUrl0": "'{0}' service endpoint URL", 6 | "loc.messages.PSLIB_EnumeratingSubdirectoriesFailedForPath0": "Enumerating subdirectories failed for path: '{0}'", 7 | "loc.messages.PSLIB_FileNotFound0": "File not found: '{0}'", 8 | "loc.messages.PSLIB_Input0": "'{0}' input", 9 | "loc.messages.PSLIB_InvalidPattern0": "Invalid pattern: '{0}'", 10 | "loc.messages.PSLIB_LeafPathNotFound0": "Leaf path not found: '{0}'", 11 | "loc.messages.PSLIB_PathLengthNotReturnedFor0": "Path normalization/expansion failed. The path length was not returned by the Kernel32 subsystem for: '{0}'", 12 | "loc.messages.PSLIB_PathNotFound0": "Path not found: '{0}'", 13 | "loc.messages.PSLIB_Process0ExitedWithCode1": "Process '{0}' exited with code '{1}'.", 14 | "loc.messages.PSLIB_Required0": "Required: {0}", 15 | "loc.messages.PSLIB_StringFormatFailed": "String format failed.", 16 | "loc.messages.PSLIB_StringResourceKeyNotFound0": "String resource key not found: '{0}'", 17 | "loc.messages.PSLIB_TaskVariable0": "'{0}' task variable" 18 | } -------------------------------------------------------------------------------- /hugo-task/Get-HugoExecutable.ps1: -------------------------------------------------------------------------------- 1 | function Get-HugoExecutable { 2 | [CmdletBinding()] 3 | param( 4 | [Parameter(Mandatory = $true)] 5 | [string]$SourceUrl, 6 | [Parameter(Mandatory = $true)] 7 | [string]$Version) 8 | 9 | Trace-VstsEnteringInvocation $MyInvocation 10 | 11 | $hugoFolder = "${env:TEMP}\hugotask_${Version}" 12 | $hugoExe = "${hugoFolder}\hugo.exe" 13 | Write-Debug "Hugo executable searched at ${hugoExe}" 14 | 15 | try { 16 | 17 | # do we have it? 18 | if (-not (Test-Path $hugoExe)) { 19 | 20 | Write-Verbose "Cached hugo version ${Version} not found: downloading from GitHub" 21 | 22 | New-Item -Path $hugoFolder -ItemType Directory -Force | Out-Null 23 | $hugoZip = "${hugoFolder}\hugo.zip" 24 | Invoke-WebRequest -Uri $SourceUrl -OutFile $hugoZip -UseBasicParsing 25 | 26 | Write-Verbose "Download complete: unzipping" 27 | 28 | [System.Reflection.Assembly]::LoadWithPartialName('System.IO.Compression.FileSystem') | Out-Null 29 | [System.IO.Compression.ZipFile]::ExtractToDirectory($hugoZip, $hugoFolder) 30 | 31 | Write-Verbose "Unzip completed: cleanup." 32 | 33 | Remove-Item $hugoZip -Force | Out-Null 34 | } 35 | return $hugoExe 36 | 37 | } finally { 38 | Trace-VstsLeavingInvocation $MyInvocation 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /hugo-task/ps_modules/VstsTaskSdk/Strings/resources.resjson/fr-fr/resources.resjson: -------------------------------------------------------------------------------- 1 | { 2 | "loc.messages.PSLIB_ContainerPathNotFound0": "Le chemin du conteneur est introuvable : '{0}'", 3 | "loc.messages.PSLIB_EndpointAuth0": "Informations d'identification du point de terminaison de service '{0}'", 4 | "loc.messages.PSLIB_EndpointUrl0": "URL du point de terminaison de service '{0}'", 5 | "loc.messages.PSLIB_EnumeratingSubdirectoriesFailedForPath0": "Échec de l'énumération des sous-répertoires pour le chemin : '{0}'", 6 | "loc.messages.PSLIB_FileNotFound0": "Fichier introuvable : {0}.", 7 | "loc.messages.PSLIB_Input0": "Entrée '{0}'", 8 | "loc.messages.PSLIB_InvalidPattern0": "Modèle non valide : '{0}'", 9 | "loc.messages.PSLIB_LeafPathNotFound0": "Le chemin feuille est introuvable : '{0}'", 10 | "loc.messages.PSLIB_PathLengthNotReturnedFor0": "Échec de la normalisation/l'expansion du chemin. La longueur du chemin n'a pas été retournée par le sous-système Kernel32 pour : '{0}'", 11 | "loc.messages.PSLIB_PathNotFound0": "Chemin introuvable : '{0}'", 12 | "loc.messages.PSLIB_Process0ExitedWithCode1": "Le processus '{0}' s'est arrêté avec le code '{1}'.", 13 | "loc.messages.PSLIB_Required0": "Obligatoire : {0}", 14 | "loc.messages.PSLIB_StringFormatFailed": "Échec du format de la chaîne.", 15 | "loc.messages.PSLIB_StringResourceKeyNotFound0": "Clé de la ressource de type chaîne introuvable : '{0}'", 16 | "loc.messages.PSLIB_TaskVariable0": "Variable de tâche '{0}'" 17 | } -------------------------------------------------------------------------------- /hugo-task/ps_modules/VstsTaskSdk/Strings/resources.resjson/it-IT/resources.resjson: -------------------------------------------------------------------------------- 1 | { 2 | "loc.messages.PSLIB_ContainerPathNotFound0": "Percorso del contenitore non trovato: '{0}'", 3 | "loc.messages.PSLIB_EndpointAuth0": "Credenziali dell'endpoint servizio '{0}'", 4 | "loc.messages.PSLIB_EndpointUrl0": "URL dell'endpoint servizio '{0}'", 5 | "loc.messages.PSLIB_EnumeratingSubdirectoriesFailedForPath0": "L'enumerazione delle sottodirectory per il percorso '{0}' non è riuscita", 6 | "loc.messages.PSLIB_FileNotFound0": "File non trovato: '{0}'", 7 | "loc.messages.PSLIB_Input0": "Input di '{0}'", 8 | "loc.messages.PSLIB_InvalidPattern0": "Criterio non valido: '{0}'", 9 | "loc.messages.PSLIB_LeafPathNotFound0": "Percorso foglia non trovato: '{0}'", 10 | "loc.messages.PSLIB_PathLengthNotReturnedFor0": "La normalizzazione o l'espansione del percorso non è riuscita. Il sottosistema Kernel32 non ha restituito la lunghezza del percorso per '{0}'", 11 | "loc.messages.PSLIB_PathNotFound0": "Percorso non trovato: '{0}'", 12 | "loc.messages.PSLIB_Process0ExitedWithCode1": "Il processo '{0}' è stato terminato ed è stato restituito il codice '{1}'.", 13 | "loc.messages.PSLIB_Required0": "Obbligatorio: {0}", 14 | "loc.messages.PSLIB_StringFormatFailed": "Errore nel formato della stringa.", 15 | "loc.messages.PSLIB_StringResourceKeyNotFound0": "La chiave della risorsa stringa non è stata trovata: '{0}'", 16 | "loc.messages.PSLIB_TaskVariable0": "Variabile dell'attività '{0}'" 17 | } -------------------------------------------------------------------------------- /hugo-task/ps_modules/VstsTaskSdk/Strings/resources.resjson/es-es/resources.resjson: -------------------------------------------------------------------------------- 1 | { 2 | "loc.messages.PSLIB_ContainerPathNotFound0": "No se encuentra la ruta de acceso del contenedor: '{0}'", 3 | "loc.messages.PSLIB_EndpointAuth0": "Credenciales del punto de conexión de servicio '{0}'", 4 | "loc.messages.PSLIB_EndpointUrl0": "URL del punto de conexión de servicio '{0}'", 5 | "loc.messages.PSLIB_EnumeratingSubdirectoriesFailedForPath0": "No se pudieron enumerar los subdirectorios de la ruta de acceso: '{0}'", 6 | "loc.messages.PSLIB_FileNotFound0": "Archivo no encontrado: '{0}'", 7 | "loc.messages.PSLIB_Input0": "Entrada '{0}'", 8 | "loc.messages.PSLIB_InvalidPattern0": "Patrón no válido: '{0}'", 9 | "loc.messages.PSLIB_LeafPathNotFound0": "No se encuentra la ruta de acceso de la hoja: '{0}'", 10 | "loc.messages.PSLIB_PathLengthNotReturnedFor0": "No se pudo normalizar o expandir la ruta de acceso. El subsistema Kernel32 no devolvió la longitud de la ruta de acceso para: '{0}'", 11 | "loc.messages.PSLIB_PathNotFound0": "No se encuentra la ruta de acceso: '{0}'", 12 | "loc.messages.PSLIB_Process0ExitedWithCode1": "El proceso '{0}' finalizó con el código '{1}'.", 13 | "loc.messages.PSLIB_Required0": "Se requiere: {0}", 14 | "loc.messages.PSLIB_StringFormatFailed": "Error de formato de cadena.", 15 | "loc.messages.PSLIB_StringResourceKeyNotFound0": "No se encuentra la clave de recurso de la cadena: '{0}'", 16 | "loc.messages.PSLIB_TaskVariable0": "Variable de tarea '{0}'" 17 | } -------------------------------------------------------------------------------- /hugo-task/ps_modules/VstsTaskSdk/Strings/resources.resjson/de-de/resources.resjson: -------------------------------------------------------------------------------- 1 | { 2 | "loc.messages.PSLIB_ContainerPathNotFound0": "Der Containerpfad wurde nicht gefunden: \"{0}\".", 3 | "loc.messages.PSLIB_EndpointAuth0": "\"{0}\"-Dienstendpunkt-Anmeldeinformationen", 4 | "loc.messages.PSLIB_EndpointUrl0": "\"{0}\"-Dienstendpunkt-URL", 5 | "loc.messages.PSLIB_EnumeratingSubdirectoriesFailedForPath0": "Fehler beim Aufzählen von Unterverzeichnissen für den folgenden Pfad: \"{0}\"", 6 | "loc.messages.PSLIB_FileNotFound0": "Die Datei wurde nicht gefunden: \"{0}\".", 7 | "loc.messages.PSLIB_Input0": "\"{0}\"-Eingabe", 8 | "loc.messages.PSLIB_InvalidPattern0": "Ungültiges Muster: \"{0}\"", 9 | "loc.messages.PSLIB_LeafPathNotFound0": "Der Blattpfad wurde nicht gefunden: \"{0}\".", 10 | "loc.messages.PSLIB_PathLengthNotReturnedFor0": "Fehler bei der Normalisierung bzw. Erweiterung des Pfads. Die Pfadlänge wurde vom Kernel32-Subsystem nicht zurückgegeben für: \"{0}\"", 11 | "loc.messages.PSLIB_PathNotFound0": "Der Pfad wurde nicht gefunden: \"{0}\".", 12 | "loc.messages.PSLIB_Process0ExitedWithCode1": "Der Prozess \"{0}\" wurde mit dem Code \"{1}\" beendet.", 13 | "loc.messages.PSLIB_Required0": "Erforderlich: {0}", 14 | "loc.messages.PSLIB_StringFormatFailed": "Fehler beim Zeichenfolgenformat.", 15 | "loc.messages.PSLIB_StringResourceKeyNotFound0": "Der Zeichenfolgen-Ressourcenschlüssel wurde nicht gefunden: \"{0}\".", 16 | "loc.messages.PSLIB_TaskVariable0": "\"{0}\"-Taskvariable" 17 | } -------------------------------------------------------------------------------- /hugo-task/Invoke-Hugo.ps1: -------------------------------------------------------------------------------- 1 | function Invoke-Hugo { 2 | [CmdletBinding()] 3 | param( 4 | [Parameter(Mandatory = $true)] 5 | [string]$hugoExePath, 6 | [Parameter(Mandatory = $true)] 7 | [string]$source, 8 | [string]$destination, 9 | [string]$baseURL, 10 | 11 | [bool]$buildDrafts, 12 | [bool]$buildExpired, 13 | [bool]$buildFuture, 14 | [bool]$uglyURLs 15 | ) 16 | 17 | Trace-VstsEnteringInvocation $MyInvocation 18 | try { 19 | 20 | $flags = "" 21 | $flags += " --source ${source}" 22 | if (![string]::IsNullOrWhiteSpace($baseURL)) { $flags += " --baseURL ${baseURL}" } 23 | if (![string]::IsNullOrWhiteSpace($destination)) { 24 | Write-Verbose "No destination specified, Hugo will assume 'public'" 25 | $flags += " --destination ${destination}" 26 | } 27 | 28 | if ($buildDrafts) { $flags += " --buildDrafts" } 29 | if ($buildExpired) { $flags += " --buildExpired" } 30 | if ($buildFuture) { $flags += " --buildFuture" } 31 | if ($uglyURLs) { $flags += " --uglyURLs" } 32 | 33 | $implicitFlags = " --enableGitInfo --i18n-warnings --verbose" 34 | 35 | Invoke-VstsTool -FileName $hugoExePath -Arguments "${flags} ${implicitFlags}" -RequireExitCodeZero 36 | #Invoke-Expression "${hugoExePath} ${flags} ${implicitFlags}" 37 | 38 | } finally { 39 | Trace-VstsLeavingInvocation $MyInvocation 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /hugo-taskV2/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "HugoTask", 3 | "description": "HugoTask", 4 | "scripts": { 5 | "build": "npm run clean && npm run lint && npm run transpile", 6 | "package": "npm prune -production && npm run copy", 7 | "___copy": "ncp ./dist/src . && rimraf ./dist", 8 | "copy": "npx mkdirp ../hugo-taskV2 && ncp ./node_modules ../hugo-taskV2/node_modules && ncp ./dist/src ../hugo-taskV2/ && copyfiles *.json *.png ../hugo-taskV2/", 9 | "clean": "rimraf ./dist && rimraf ./src/*.js ./src/*.js.map ./*.js ./*.js.map && rimraf ./node_modules", 10 | "transpile": "tsc -p .", 11 | "lint": "npm install @typescript-eslint/eslint-plugin@latest --save-dev && eslint . --ext .js,.jsx,.ts,.tsx", 12 | "test": "mocha -r ts-node/register ./test/*.ts --reporter mocha-junit-reporter --reporter-options mochaFile=./test-output/test-results.xml ", 13 | "test-no-logger": "mocha -r ts-node/register ./test/*.ts " 14 | }, 15 | "main": "src/HugoTask.js", 16 | "dependencies": { 17 | "@types/node": "^12.22.0", 18 | "@types/mocha": "^5.2.7", 19 | "azure-pipelines-tool-lib": "^1.0.2", 20 | "azure-pipelines-task-lib": "^3.1.10", 21 | "azure-pipelines-tasks-utility-common": "^3.0.3" 22 | }, 23 | "devDependencies": { 24 | "@typescript-eslint/eslint-plugin": "^5.8.0", 25 | "@typescript-eslint/parser": "^5.8.0", 26 | "copyfiles": "latest", 27 | "eslint": "^8.5.0", 28 | "typescript": "^4.5.4" 29 | }, 30 | "author": "Giulio Vian", 31 | "license": "MIT", 32 | "repository": { 33 | "type": "git", 34 | "url": "git://github.com/giuliov/hugo-vsts-extension.git" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /hugo-task/HugoTask.ps1: -------------------------------------------------------------------------------- 1 | [CmdletBinding()] 2 | param() 3 | 4 | Trace-VstsEnteringInvocation $MyInvocation 5 | try { 6 | # Add Localization: 7 | # Import-VstsLocStrings "$PSScriptRoot\Task.json" 8 | 9 | # Get the inputs. 10 | [string]$source = Get-VstsInput -Name Source 11 | [string]$destination = Get-VstsInput -Name Destination 12 | [string]$hugoVersion = Get-VstsInput -Name HugoVersion 13 | [bool]$extendedVersion = Get-VstsInput -Name ExtendedVersion -AsBool 14 | [string]$baseURL = Get-VstsInput -Name BaseURL 15 | 16 | [bool]$buildDrafts = Get-VstsInput -Name BuildDrafts -AsBool 17 | [bool]$buildExpired = Get-VstsInput -Name BuildExpired -AsBool 18 | [bool]$buildFuture = Get-VstsInput -Name BuildFuture -AsBool 19 | [bool]$uglyURLs = Get-VstsInput -Name UglyURLs -AsBool 20 | 21 | Assert-VstsPath -LiteralPath $source -PathType Container 22 | 23 | # Import the helpers. 24 | $here = $PSScriptRoot # this helps in quick debugging 25 | . $here\Select-HugoVersion.ps1 26 | . $here\Get-HugoExecutable.ps1 27 | . $here\Invoke-Hugo.ps1 28 | 29 | 30 | $versionInfo = Select-HugoVersion -PreferredVersion $hugoVersion -ExtendedVersion $extendedVersion 31 | if (!$versionInfo) { 32 | Write-Error "Something bad happened while querying Hugo versions in GitHub" 33 | return 34 | } 35 | [string]$hugoExePath = Get-HugoExecutable -SourceUrl $versionInfo.DownloadURL -Version $versionInfo.Version 36 | Invoke-Hugo -hugoExePath $hugoExePath -source $source -destination $destination -baseURL $baseURL -buildDrafts $buildDrafts -buildExpired $buildExpired -buildFuture $buildFuture -uglyURLs $uglyURLs 37 | 38 | 39 | } finally { 40 | Trace-VstsLeavingInvocation $MyInvocation 41 | } -------------------------------------------------------------------------------- /hugo-task/Select-HugoVersion.ps1: -------------------------------------------------------------------------------- 1 | function Select-HugoVersion 2 | { 3 | [CmdletBinding()] 4 | param( 5 | [Parameter(Mandatory = $true)] 6 | [string] $PreferredVersion, 7 | [Parameter(Mandatory = $false)] 8 | [bool] $ExtendedVersion = $false 9 | ) 10 | 11 | Trace-VstsEnteringInvocation $MyInvocation 12 | try { 13 | 14 | Write-Verbose "Querying Hugo GitHub releases" 15 | 16 | # ask GitHub for Releases 17 | if ($PreferredVersion -eq "latest") { 18 | $availableReleases = Invoke-GitHubAPI "/repos/gohugoio/hugo/releases?draft=false" 19 | } else { 20 | $availableReleases = Invoke-GitHubAPI "/repos/gohugoio/hugo/releases/tags/v${PreferredVersion}" 21 | } 22 | $release = $availableReleases | where { (-not $_.draft) -and (-not $_.prerelease) } | select id,name,assets -First 1 23 | 24 | Write-Verbose "Found release $( $release.name )" 25 | 26 | $win64hugo = $release.assets | where { $_.name -like '*Windows*64*' } 27 | 28 | # extended since 0.43 29 | $DownloadURL = $win64hugo.browser_download_url 30 | if ($DownloadURL -isnot [string]) { 31 | if ($ExtendedVersion) { 32 | $DownloadURL = $DownloadURL | where { $_ -like '*extended*' } 33 | } else { 34 | $DownloadURL = $DownloadURL | where { $_ -notlike '*extended*' } 35 | } 36 | } 37 | 38 | return @{ Version = $release.name; DownloadURL = $DownloadURL } 39 | 40 | } finally { 41 | Trace-VstsLeavingInvocation $MyInvocation 42 | } 43 | } 44 | 45 | 46 | function Invoke-GitHubAPI($api) { 47 | # github api now only accepts TLS 1.2, see https://githubengineering.com/crypto-removal-notice/ 48 | [System.Net.ServicePointManager]::SecurityProtocol = @("Tls12") 49 | # stick with v3 API 50 | $x = Invoke-WebRequest -Uri "https://api.github.com${api}" -Headers @{"accept"="application/vnd.github.v3+json"} -UseBasicParsing 51 | return $x.Content | ConvertFrom-Json 52 | } 53 | 54 | <# 55 | Select-HugoVersion 'latest' $false 56 | Select-HugoVersion 'latest' $true 57 | Select-HugoVersion '0.43' $false 58 | Select-HugoVersion '0.43' $true 59 | Select-HugoVersion '0.31' $false 60 | Select-HugoVersion '0.31' $true 61 | #> -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Generates a site using Hugo, a Fast and Flexible Website Generator. 2 | 3 | [![Hugo logo here](images/hugo-logo.png)](https://gohugo.io/) 4 | 5 | # Usage 6 | 7 | The Build Task will automatically download the `hugo` executable, if the version is not already present on the build machine, and invoke it. 8 | 9 | You can specify some common options. 10 | 11 | - **Source**: relative path from repo root of the Hugo sources, defaults to `Build.SourcesDirectory`, passed as `--source` flag. 12 | - **Destination**: path of Hugo generated site, typically `Build.ArtifactStagingDirectory`, passed as `--destination` flag. 13 | - **Hugo Version**: defines the Hugo version, use `latest`, `0.25.1`, `0.24`, but not `v0.24` (pick valid values from Hugo [Releases](https://github.com/gohugoio/hugo/releases) page). If the preferred version cannot be found, the latest released version will be used instead. 14 | - **Extended Version**: download the extended Hugo version (SCSS/SASS support). 15 | - **Base URL**: sets the hostname (and path) to the root, e.g. `http://example.com/`, passed as `--baseURL` flag. 16 | - **Include Drafts**: to include content marked as draft, passed as `--buildDrafts` flag. 17 | - **Include Expired**: to include expired content, passed as `--buildExpired` flag. 18 | - **Include Future**: to include content with publishdate in the future, passed as `--buildFuture` flag. 19 | - ~~**Use Ugly URLs**: to use `/filename.html` instead of `/filename/`, passed as `--uglyURLs` flag.~~ 20 | 21 | ![Build Task Arguments screenshot here](images/BuildTaskArguments.png) 22 | 23 | A detailed walk through is here [Building a Hugo Site with the Azure DevOps Extension](hugo-extension-step-by-step.md) 24 | 25 | More Information on Hugo on [this site](https://gohugo.io/). 26 | 27 | The Hugo logos are copyright © Steve Francia 2013–2022. 28 | 29 | # Release Notes 30 | 31 | ## 2.0 32 | 33 | - New Cross-platform implementation. 34 | - Removed `uglyURLs` option, deprecated since Hugo 0.xx ??? 35 | - Added `additionalArgs` option to allow custom arguments. 36 | 37 | ## 1.1.0 38 | 39 | - Fixes ([ISSUE#5](https://github.com/giuliov/hugo-vsts-extension/issues/5)) 40 | - Added support for Extended version (Hugo 0.43 and later) 41 | 42 | ## 1.0.1 43 | 44 | - Use TLS 1.2 for GitHub API ([PR#3](https://github.com/giuliov/hugo-vsts-extension/pull/3)) 45 | 46 | ## 1.0.0 47 | 48 | - Initial release 49 | -------------------------------------------------------------------------------- /vss-extension.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifestVersion": 1, 3 | "id": "hugo-extension", 4 | "version": "2.0.5", 5 | "name": "Hugo", 6 | "publisher": "giuliovdev", 7 | "description": "Generates a static web site using Hugo, a Fast and Flexible Website Generator.", 8 | "targets": [ 9 | { 10 | "id": "Microsoft.VisualStudio.Services" 11 | } 12 | ], 13 | "galleryFlags": [ 14 | "Public" 15 | ], 16 | "icons": { 17 | "default": "img/hugo-128x128.png" 18 | }, 19 | "scopes": [ 20 | "vso.build_execute", 21 | "vso.release_execute" 22 | ], 23 | "categories": [ 24 | "Azure Pipelines" 25 | ], 26 | "tags": [ 27 | "hugo", 28 | "static generator", 29 | "build" 30 | ], 31 | "branding": { 32 | "color": "rgb(10,25,34)", 33 | "theme": "dark" 34 | }, 35 | "content": { 36 | "details": { 37 | "path": "README.md" 38 | }, 39 | "license": { 40 | "path": "LICENSE.txt" 41 | } 42 | }, 43 | "links": { 44 | "getstarted": { 45 | "uri": "https://github.com/giuliov/hugo-vsts-extension/blob/master/README.md" 46 | }, 47 | "support": { 48 | "uri": "https://github.com/giuliov/hugo-vsts-extension/issues" 49 | } 50 | }, 51 | "repository": { 52 | "type": "git", 53 | "uri": "https://github.com/giuliov/hugo-vsts-extension" 54 | }, 55 | "contributions": [ 56 | { 57 | "id": "hugo-build-task", 58 | "type": "ms.vss-distributed-task.task", 59 | "targets": [ 60 | "ms.vss-distributed-task.tasks" 61 | ], 62 | "properties": { 63 | "name": "hugo-task" 64 | } 65 | }, 66 | { 67 | "id": "hugo-build-task-v2", 68 | "type": "ms.vss-distributed-task.task", 69 | "targets": [ 70 | "ms.vss-distributed-task.tasks" 71 | ], 72 | "properties": { 73 | "name": "hugo-taskV2" 74 | } 75 | } 76 | ], 77 | "files": [ 78 | { 79 | "path": "hugo-task", 80 | "addressable": false 81 | }, 82 | { 83 | "path": "hugo-taskV2", 84 | "addressable": false 85 | }, 86 | { 87 | "path": "images", 88 | "addressable": true 89 | } 90 | ] 91 | } -------------------------------------------------------------------------------- /hugo-task/ps_modules/VstsTaskSdk/OutFunctions.ps1: -------------------------------------------------------------------------------- 1 | # TODO: It would be better if the Out-Default function resolved the underlying Out-Default 2 | # command in the begin block. This would allow for supporting other modules that override 3 | # Out-Default. 4 | $script:outDefaultCmdlet = $ExecutionContext.InvokeCommand.GetCmdlet("Microsoft.PowerShell.Core\Out-Default") 5 | 6 | ######################################## 7 | # Public functions. 8 | ######################################## 9 | function Out-Default { 10 | [CmdletBinding(ConfirmImpact = "Medium")] 11 | param( 12 | [Parameter(ValueFromPipeline = $true)] 13 | [System.Management.Automation.PSObject]$InputObject) 14 | 15 | begin { 16 | #Write-Host '[Entering Begin Out-Default]' 17 | $__sp = { & $script:outDefaultCmdlet @PSBoundParameters }.GetSteppablePipeline() 18 | $__sp.Begin($pscmdlet) 19 | #Write-Host '[Leaving Begin Out-Default]' 20 | } 21 | 22 | process { 23 | #Write-Host '[Entering Process Out-Default]' 24 | if ($_ -is [System.Management.Automation.ErrorRecord]) { 25 | Write-Verbose -Message 'Error record:' 4>&1 | Out-Default 26 | Write-Verbose -Message (Remove-TrailingNewLine (Out-String -InputObject $_ -Width 2147483647)) 4>&1 | Out-Default 27 | Write-Verbose -Message 'Script stack trace:' 4>&1 | Out-Default 28 | Write-Verbose -Message "$($_.ScriptStackTrace)" 4>&1 | Out-Default 29 | Write-Verbose -Message 'Exception:' 4>&1 | Out-Default 30 | Write-Verbose -Message $_.Exception.ToString() 4>&1 | Out-Default 31 | Write-TaskError -Message $_.Exception.Message 32 | } elseif ($_ -is [System.Management.Automation.WarningRecord]) { 33 | Write-TaskWarning -Message (Remove-TrailingNewLine (Out-String -InputObject $_ -Width 2147483647)) 34 | } elseif ($_ -is [System.Management.Automation.VerboseRecord] -and !$global:__vstsNoOverrideVerbose) { 35 | foreach ($private:str in (Format-DebugMessage -Object $_)) { 36 | Write-TaskVerbose -Message $private:str 37 | } 38 | } elseif ($_ -is [System.Management.Automation.DebugRecord] -and !$global:__vstsNoOverrideVerbose) { 39 | foreach ($private:str in (Format-DebugMessage -Object $_)) { 40 | Write-TaskDebug -Message $private:str 41 | } 42 | } else { 43 | # TODO: Consider using out-string here to control the width. As a security precaution it would actually be best to set it to max so wrapping doesn't interfere with secret masking. 44 | $__sp.Process($_) 45 | } 46 | 47 | #Write-Host '[Leaving Process Out-Default]' 48 | } 49 | 50 | end { 51 | #Write-Host '[Entering End Out-Default]' 52 | $__sp.End() 53 | #Write-Host '[Leaving End Out-Default]' 54 | } 55 | } 56 | 57 | ######################################## 58 | # Private functions. 59 | ######################################## 60 | function Format-DebugMessage { 61 | [CmdletBinding()] 62 | param([psobject]$Object) 63 | 64 | $private:str = Out-String -InputObject $Object -Width 2147483647 65 | $private:str = Remove-TrailingNewLine $private:str 66 | "$private:str".Replace("`r`n", "`n").Replace("`r", "`n").Split("`n"[0]) 67 | } 68 | 69 | function Remove-TrailingNewLine { 70 | [CmdletBinding()] 71 | param($Str) 72 | if ([object]::ReferenceEquals($Str, $null)) { 73 | return $Str 74 | } elseif ($Str.EndsWith("`r`n")) { 75 | return $Str.Substring(0, $Str.Length - 2) 76 | } else { 77 | return $Str 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /hugo-task/task.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "5F838DA4-D713-497A-BD7A-4987F25E3C2F", 3 | "name": "HugoTask", 4 | "friendlyName": "Hugo (Windows-only)", 5 | "description": "Generate a static web site using Hugo, a Fast and Flexible Website Generator (Windows-only)", 6 | "author": "Giulio Vian", 7 | "helpMarkDown": "[More Information on Task](https://github.com/giuliov/hugo-vsts-extension/blob/master/README.md), for [Information on Hugo](https://gohugo.io/).", 8 | "category": "Build", 9 | "visibility": [ 10 | "Build" 11 | ], 12 | "demands": [], 13 | "version": { 14 | "Major": 1, 15 | "Minor": 1, 16 | "Patch": 2 17 | }, 18 | "minimumAgentVersion": "2.105.7", 19 | "instanceNameFormat": "Hugo generate $(source)", 20 | "groups": [ 21 | { 22 | "name": "advanced", 23 | "displayName": "Advanced", 24 | "isExpanded": false 25 | } 26 | ], 27 | "inputs": [ 28 | { 29 | "name": "source", 30 | "type": "filePath", 31 | "label": "Source", 32 | "defaultValue": "", 33 | "required": false, 34 | "helpMarkDown": "Relative path from repo root of the Hugo sources." 35 | }, 36 | { 37 | "name": "destination", 38 | "type": "filePath", 39 | "label": "Destination", 40 | "defaultValue": "", 41 | "required": true, 42 | "helpMarkDown": "Path of Hugo generated site." 43 | }, 44 | { 45 | "name": "hugoVersion", 46 | "type": "string", 47 | "label": "Hugo Version", 48 | "required": false, 49 | "helpMarkDown": "If the preferred version cannot be found, the latest version found will be used instead. Use `latest`, `0.25.1`, `0.24`, but not `v0.24`.", 50 | "defaultValue": "latest" 51 | }, 52 | { 53 | "name": "extendedVersion", 54 | "type": "boolean", 55 | "label": "Extended Version", 56 | "defaultValue": false, 57 | "required": false, 58 | "helpMarkDown": "Download SCSS/SASS support." 59 | }, 60 | { 61 | "name": "baseURL", 62 | "type": "string", 63 | "label": "Base URL", 64 | "defaultValue": "", 65 | "required": false, 66 | "helpMarkDown": "Hostname (and path) to the root, e.g. http://spf13.com/." 67 | }, 68 | { 69 | "name": "buildDrafts", 70 | "type": "boolean", 71 | "label": "Include Drafts", 72 | "defaultValue": "false", 73 | "required": false, 74 | "helpMarkDown": "Include content marked as draft." 75 | }, 76 | { 77 | "name": "buildExpired", 78 | "type": "boolean", 79 | "label": "Include Expired", 80 | "defaultValue": "false", 81 | "required": false, 82 | "helpMarkDown": "Include expired content." 83 | }, 84 | { 85 | "name": "buildFuture", 86 | "type": "boolean", 87 | "label": "Include Future", 88 | "defaultValue": "false", 89 | "required": false, 90 | "helpMarkDown": "Include content with publishdate in the future." 91 | }, 92 | { 93 | "name": "uglyURLs", 94 | "type": "boolean", 95 | "label": "Use Ugly URLs", 96 | "defaultValue": "false", 97 | "required": false, 98 | "helpMarkDown": "If true, use /filename.html instead of /filename/." 99 | } 100 | ], 101 | "execution": { 102 | "PowerShell3": { 103 | "target": "HugoTask.ps1" 104 | } 105 | } 106 | } -------------------------------------------------------------------------------- /hugo-taskV2/task.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "528c73d9-c552-4e2d-a26f-fb5c91c32554", 3 | "name": "HugoTask", 4 | "friendlyName": "Hugo", 5 | "description": "Generate a static web site using Hugo, a Fast and Flexible Website Generator (cross-platform task).", 6 | "author": "Giulio Vian", 7 | "helpMarkDown": "[More Information on Task](https://github.com/giuliov/hugo-vsts-extension/blob/master/README.md), for [Information on Hugo](https://gohugo.io/).", 8 | "category": "Build", 9 | "visibility": [ 10 | "Build", 11 | "Release" 12 | ], 13 | "demands": [], 14 | "version": { 15 | "Major": 2, 16 | "Minor": 0, 17 | "Patch": 0 18 | }, 19 | "minimumAgentVersion": "2.105.7", 20 | "instanceNameFormat": "Hugo generate $(source)", 21 | "groups": [ 22 | { 23 | "name": "advanced", 24 | "displayName": "Advanced", 25 | "isExpanded": false 26 | } 27 | ], 28 | "inputs": [ 29 | { 30 | "name": "source", 31 | "type": "filePath", 32 | "label": "Source", 33 | "defaultValue": "", 34 | "required": false, 35 | "helpMarkDown": "Relative path from repo root of the Hugo sources." 36 | }, 37 | { 38 | "name": "destination", 39 | "type": "filePath", 40 | "label": "Destination", 41 | "defaultValue": "", 42 | "required": true, 43 | "helpMarkDown": "Path of Hugo generated site." 44 | }, 45 | { 46 | "name": "hugoVersion", 47 | "type": "string", 48 | "label": "Hugo Version", 49 | "required": false, 50 | "helpMarkDown": "If the preferred version cannot be found, the latest version found will be used instead. Use `latest`, `0.25.1`, `0.24`, but not `v0.24`.", 51 | "defaultValue": "latest" 52 | }, 53 | { 54 | "name": "extendedVersion", 55 | "type": "boolean", 56 | "label": "Extended Version", 57 | "defaultValue": false, 58 | "required": false, 59 | "helpMarkDown": "Download SCSS/SASS support." 60 | }, 61 | { 62 | "name": "baseURL", 63 | "type": "string", 64 | "label": "Base URL", 65 | "defaultValue": "", 66 | "required": false, 67 | "helpMarkDown": "Hostname (and path) to the root, e.g. http://spf13.com/." 68 | }, 69 | { 70 | "name": "buildDrafts", 71 | "type": "boolean", 72 | "label": "Include Drafts", 73 | "defaultValue": "false", 74 | "required": false, 75 | "helpMarkDown": "Include content marked as draft." 76 | }, 77 | { 78 | "name": "buildExpired", 79 | "type": "boolean", 80 | "label": "Include Expired", 81 | "defaultValue": "false", 82 | "required": false, 83 | "helpMarkDown": "Include expired content." 84 | }, 85 | { 86 | "name": "buildFuture", 87 | "type": "boolean", 88 | "label": "Include Future", 89 | "defaultValue": "false", 90 | "required": false, 91 | "helpMarkDown": "Include content with publishdate in the future." 92 | }, 93 | { 94 | "name": "additionalArgs", 95 | "type": "string", 96 | "label": "Any additional argument", 97 | "defaultValue": "", 98 | "required": false, 99 | "helpMarkDown": "Additional arguments to pass to Hugo." 100 | } 101 | ], 102 | "execution": { 103 | "Node10": { 104 | "target": "HugoTask.js", 105 | "argumentFormat": "" 106 | } 107 | } 108 | } -------------------------------------------------------------------------------- /hugo-task/ps_modules/VstsTaskSdk/ToolFunctions.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Asserts the agent version is at least the specified minimum. 4 | 5 | .PARAMETER Minimum 6 | Minimum version - must be 2.104.1 or higher. 7 | #> 8 | function Assert-Agent { 9 | [CmdletBinding()] 10 | param( 11 | [Parameter(Mandatory = $true)] 12 | [version]$Minimum) 13 | 14 | if (([version]'2.104.1').CompareTo($Minimum) -ge 1) { 15 | Write-Error "Assert-Agent requires the parameter to be 2.104.1 or higher." 16 | return 17 | } 18 | 19 | $agent = Get-TaskVariable -Name 'agent.version' 20 | if (!$agent -or $Minimum.CompareTo([version]$agent) -ge 1) { 21 | Write-Error (Get-LocString -Key 'PSLIB_AgentVersion0Required' -ArgumentList $Minimum) 22 | } 23 | } 24 | 25 | <# 26 | .SYNOPSIS 27 | Asserts that a path exists. Throws if the path does not exist. 28 | 29 | .PARAMETER PassThru 30 | True to return the path. 31 | #> 32 | function Assert-Path { 33 | [CmdletBinding()] 34 | param( 35 | [Parameter(Mandatory = $true)] 36 | [string]$LiteralPath, 37 | [Microsoft.PowerShell.Commands.TestPathType]$PathType = [Microsoft.PowerShell.Commands.TestPathType]::Any, 38 | [switch]$PassThru) 39 | 40 | if ($PathType -eq [Microsoft.PowerShell.Commands.TestPathType]::Any) { 41 | Write-Verbose "Asserting path exists: '$LiteralPath'" 42 | } else { 43 | Write-Verbose "Asserting $("$PathType".ToLowerInvariant()) path exists: '$LiteralPath'" 44 | } 45 | 46 | if (Test-Path -LiteralPath $LiteralPath -PathType $PathType) { 47 | if ($PassThru) { 48 | return $LiteralPath 49 | } 50 | 51 | return 52 | } 53 | 54 | $resourceKey = switch ($PathType) { 55 | ([Microsoft.PowerShell.Commands.TestPathType]::Container) { "PSLIB_ContainerPathNotFound0" ; break } 56 | ([Microsoft.PowerShell.Commands.TestPathType]::Leaf) { "PSLIB_LeafPathNotFound0" ; break } 57 | default { "PSLIB_PathNotFound0" } 58 | } 59 | 60 | throw (Get-LocString -Key $resourceKey -ArgumentList $LiteralPath) 61 | } 62 | 63 | <# 64 | .SYNOPSIS 65 | Executes an external program. 66 | 67 | .DESCRIPTION 68 | Executes an external program and waits for the process to exit. 69 | 70 | After calling this command, the exit code of the process can be retrieved from the variable $LASTEXITCODE. 71 | 72 | .PARAMETER Encoding 73 | This parameter not required for most scenarios. Indicates how to interpret the encoding from the external program. An example use case would be if an external program outputs UTF-16 XML and the output needs to be parsed. 74 | 75 | .PARAMETER RequireExitCodeZero 76 | Indicates whether to write an error to the error pipeline if the exit code is not zero. 77 | #> 78 | function Invoke-Tool { # TODO: RENAME TO INVOKE-PROCESS? 79 | [CmdletBinding()] 80 | param( 81 | [ValidatePattern('^[^\r\n]*$')] 82 | [Parameter(Mandatory = $true)] 83 | [string]$FileName, 84 | [ValidatePattern('^[^\r\n]*$')] 85 | [Parameter()] 86 | [string]$Arguments, 87 | [string]$WorkingDirectory, 88 | [System.Text.Encoding]$Encoding, 89 | [switch]$RequireExitCodeZero) 90 | 91 | Trace-EnteringInvocation $MyInvocation 92 | $isPushed = $false 93 | $originalEncoding = $null 94 | try { 95 | if ($Encoding) { 96 | $originalEncoding = [System.Console]::OutputEncoding 97 | [System.Console]::OutputEncoding = $Encoding 98 | } 99 | 100 | if ($WorkingDirectory) { 101 | Push-Location -LiteralPath $WorkingDirectory -ErrorAction Stop 102 | $isPushed = $true 103 | } 104 | 105 | $FileName = $FileName.Replace('"', '').Replace("'", "''") 106 | Write-Host "##[command]""$FileName"" $Arguments" 107 | Invoke-Expression "& '$FileName' --% $Arguments" 108 | Write-Verbose "Exit code: $LASTEXITCODE" 109 | if ($RequireExitCodeZero -and $LASTEXITCODE -ne 0) { 110 | Write-Error (Get-LocString -Key PSLIB_Process0ExitedWithCode1 -ArgumentList ([System.IO.Path]::GetFileName($FileName)), $LASTEXITCODE) 111 | } 112 | } finally { 113 | if ($originalEncoding) { 114 | [System.Console]::OutputEncoding = $originalEncoding 115 | } 116 | 117 | if ($isPushed) { 118 | Pop-Location 119 | } 120 | 121 | Trace-LeavingInvocation $MyInvocation 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /hugo-task/ps_modules/VstsTaskSdk/TraceFunctions.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Writes verbose information about the invocation being entered. 4 | 5 | .DESCRIPTION 6 | Used to trace verbose information when entering a function/script. Writes an entering message followed by a short description of the invocation. Additionally each bound parameter and unbound argument is also traced. 7 | 8 | .PARAMETER Parameter 9 | Wildcard pattern to control which bound parameters are traced. 10 | #> 11 | function Trace-EnteringInvocation { 12 | [CmdletBinding()] 13 | param( 14 | [Parameter(Mandatory = $true)] 15 | [System.Management.Automation.InvocationInfo]$InvocationInfo, 16 | [string[]]$Parameter = '*') 17 | 18 | Write-Verbose "Entering $(Get-InvocationDescription $InvocationInfo)." 19 | $OFS = ", " 20 | if ($InvocationInfo.BoundParameters.Count -and $Parameter.Count) { 21 | if ($Parameter.Count -eq 1 -and $Parameter[0] -eq '*') { 22 | # Trace all parameters. 23 | foreach ($key in $InvocationInfo.BoundParameters.Keys) { 24 | Write-Verbose " $($key): '$($InvocationInfo.BoundParameters[$key])'" 25 | } 26 | } else { 27 | # Trace matching parameters. 28 | foreach ($key in $InvocationInfo.BoundParameters.Keys) { 29 | foreach ($p in $Parameter) { 30 | if ($key -like $p) { 31 | Write-Verbose " $($key): '$($InvocationInfo.BoundParameters[$key])'" 32 | break 33 | } 34 | } 35 | } 36 | } 37 | } 38 | 39 | # Trace all unbound arguments. 40 | if (@($InvocationInfo.UnboundArguments).Count) { 41 | for ($i = 0 ; $i -lt $InvocationInfo.UnboundArguments.Count ; $i++) { 42 | Write-Verbose " args[$i]: '$($InvocationInfo.UnboundArguments[$i])'" 43 | } 44 | } 45 | } 46 | 47 | <# 48 | .SYNOPSIS 49 | Writes verbose information about the invocation being left. 50 | 51 | .DESCRIPTION 52 | Used to trace verbose information when leaving a function/script. Writes a leaving message followed by a short description of the invocation. 53 | #> 54 | function Trace-LeavingInvocation { 55 | [CmdletBinding()] 56 | param( 57 | [Parameter(Mandatory = $true)] 58 | [System.Management.Automation.InvocationInfo]$InvocationInfo) 59 | 60 | Write-Verbose "Leaving $(Get-InvocationDescription $InvocationInfo)." 61 | } 62 | 63 | <# 64 | .SYNOPSIS 65 | Writes verbose information about paths. 66 | 67 | .DESCRIPTION 68 | Writes verbose information about the paths. The paths are sorted and a the common root is written only once, followed by each relative path. 69 | 70 | .PARAMETER PassThru 71 | Indicates whether to return the sorted paths. 72 | #> 73 | function Trace-Path { 74 | [CmdletBinding()] 75 | param( 76 | [string[]]$Path, 77 | [switch]$PassThru) 78 | 79 | if ($Path.Count -eq 0) { 80 | Write-Verbose "No paths." 81 | if ($PassThru) { 82 | $Path 83 | } 84 | } elseif ($Path.Count -eq 1) { 85 | Write-Verbose "Path: $($Path[0])" 86 | if ($PassThru) { 87 | $Path 88 | } 89 | } else { 90 | # Find the greatest common root. 91 | $sorted = $Path | Sort-Object 92 | $firstPath = $sorted[0].ToCharArray() 93 | $lastPath = $sorted[-1].ToCharArray() 94 | $commonEndIndex = 0 95 | $j = if ($firstPath.Length -lt $lastPath.Length) { $firstPath.Length } else { $lastPath.Length } 96 | for ($i = 0 ; $i -lt $j ; $i++) { 97 | if ($firstPath[$i] -eq $lastPath[$i]) { 98 | if ($firstPath[$i] -eq '\') { 99 | $commonEndIndex = $i 100 | } 101 | } else { 102 | break 103 | } 104 | } 105 | 106 | if ($commonEndIndex -eq 0) { 107 | # No common root. 108 | Write-Verbose "Paths:" 109 | foreach ($p in $sorted) { 110 | Write-Verbose " $p" 111 | } 112 | } else { 113 | Write-Verbose "Paths: $($Path[0].Substring(0, $commonEndIndex + 1))" 114 | foreach ($p in $sorted) { 115 | Write-Verbose " $($p.Substring($commonEndIndex + 1))" 116 | } 117 | } 118 | 119 | if ($PassThru) { 120 | $sorted 121 | } 122 | } 123 | } 124 | 125 | ######################################## 126 | # Private functions. 127 | ######################################## 128 | function Get-InvocationDescription { 129 | [CmdletBinding()] 130 | param([System.Management.Automation.InvocationInfo]$InvocationInfo) 131 | 132 | if ($InvocationInfo.MyCommand.Path) { 133 | $InvocationInfo.MyCommand.Path 134 | } elseif ($InvocationInfo.MyCommand.Name) { 135 | $InvocationInfo.MyCommand.Name 136 | } else { 137 | $InvocationInfo.MyCommand.CommandType 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /hugo-task/ps_modules/VstsTaskSdk/LocalizationFunctions.ps1: -------------------------------------------------------------------------------- 1 | $script:resourceStrings = @{ } 2 | 3 | <# 4 | .SYNOPSIS 5 | Gets a localized resource string. 6 | 7 | .DESCRIPTION 8 | Gets a localized resource string and optionally formats the string with arguments. 9 | 10 | If the format fails (due to a bad format string or incorrect expected arguments in the format string), then the format string is returned followed by each of the arguments (delimited by a space). 11 | 12 | If the lookup key is not found, then the lookup key is returned followed by each of the arguments (delimited by a space). 13 | 14 | .PARAMETER Require 15 | Writes an error to the error pipeline if the endpoint is not found. 16 | #> 17 | function Get-LocString { 18 | [CmdletBinding()] 19 | param( 20 | [Parameter(Mandatory = $true, Position = 1)] 21 | [string]$Key, 22 | [Parameter(Position = 2)] 23 | [object[]]$ArgumentList = @( )) 24 | 25 | # Due to the dynamically typed nature of PowerShell, a single null argument passed 26 | # to an array parameter is interpreted as a null array. 27 | if ([object]::ReferenceEquals($null, $ArgumentList)) { 28 | $ArgumentList = @( $null ) 29 | } 30 | 31 | # Lookup the format string. 32 | $format = '' 33 | if (!($format = $script:resourceStrings[$Key])) { 34 | # Warn the key was not found. Prevent recursion if the lookup key is the 35 | # "string resource key not found" lookup key. 36 | $resourceNotFoundKey = 'PSLIB_StringResourceKeyNotFound0' 37 | if ($key -ne $resourceNotFoundKey) { 38 | Write-Warning (Get-LocString -Key $resourceNotFoundKey -ArgumentList $Key) 39 | } 40 | 41 | # Fallback to just the key itself if there aren't any arguments to format. 42 | if (!$ArgumentList.Count) { return $key } 43 | 44 | # Otherwise fallback to the key followed by the arguments. 45 | $OFS = " " 46 | return "$key $ArgumentList" 47 | } 48 | 49 | # Return the string if there aren't any arguments to format. 50 | if (!$ArgumentList.Count) { return $format } 51 | 52 | try { 53 | [string]::Format($format, $ArgumentList) 54 | } catch { 55 | Write-Warning (Get-LocString -Key 'PSLIB_StringFormatFailed') 56 | $OFS = " " 57 | "$format $ArgumentList" 58 | } 59 | } 60 | 61 | <# 62 | .SYNOPSIS 63 | Imports resource strings for use with Get-VstsLocString. 64 | 65 | .DESCRIPTION 66 | Imports resource strings for use with Get-VstsLocString. The imported strings are stored in an internal resource string dictionary. Optionally, if a separate resource file for the current culture exists, then the localized strings from that file then imported (overlaid) into the same internal resource string dictionary. 67 | 68 | Resource strings from the SDK are prefixed with "PSLIB_". This prefix should be avoided for custom resource strings. 69 | 70 | .Parameter LiteralPath 71 | JSON file containing resource strings. 72 | 73 | .EXAMPLE 74 | Import-VstsLocStrings -LiteralPath $PSScriptRoot\Task.json 75 | 76 | Imports strings from messages section in the JSON file. If a messages section is not defined, then no strings are imported. Example messages section: 77 | { 78 | "messages": { 79 | "Hello": "Hello you!", 80 | "Hello0": "Hello {0}!" 81 | } 82 | } 83 | 84 | .EXAMPLE 85 | Import-VstsLocStrings -LiteralPath $PSScriptRoot\Task.json 86 | 87 | Overlays strings from an optional separate resource file for the current culture. 88 | 89 | Given the task variable System.Culture is set to 'de-DE'. This variable is set by the agent based on the current culture for the job. 90 | Given the file Task.json contains: 91 | { 92 | "messages": { 93 | "GoodDay": "Good day!", 94 | } 95 | } 96 | Given the file resources.resjson\de-DE\resources.resjson: 97 | { 98 | "loc.messages.GoodDay": "Guten Tag!" 99 | } 100 | 101 | The net result from the import command would be one new key-value pair added to the internal dictionary: Key = 'GoodDay', Value = 'Guten Tag!' 102 | #> 103 | function Import-LocStrings { 104 | [CmdletBinding()] 105 | param( 106 | [Parameter(Mandatory = $true)] 107 | [string]$LiteralPath) 108 | 109 | # Validate the file exists. 110 | if (!(Test-Path -LiteralPath $LiteralPath -PathType Leaf)) { 111 | Write-Warning (Get-LocString -Key PSLIB_FileNotFound0 -ArgumentList $LiteralPath) 112 | return 113 | } 114 | 115 | # Load the json. 116 | Write-Verbose "Loading resource strings from: $LiteralPath" 117 | $count = 0 118 | if ($messages = (Get-Content -LiteralPath $LiteralPath -Encoding UTF8 | Out-String | ConvertFrom-Json).messages) { 119 | # Add each resource string to the hashtable. 120 | foreach ($member in (Get-Member -InputObject $messages -MemberType NoteProperty)) { 121 | [string]$key = $member.Name 122 | $script:resourceStrings[$key] = $messages."$key" 123 | $count++ 124 | } 125 | } 126 | 127 | Write-Verbose "Loaded $count strings." 128 | 129 | # Get the culture. 130 | $culture = Get-TaskVariable -Name "System.Culture" -Default "en-US" 131 | 132 | # Load the resjson. 133 | $resjsonPath = "$([System.IO.Path]::GetDirectoryName($LiteralPath))\Strings\resources.resjson\$culture\resources.resjson" 134 | if (Test-Path -LiteralPath $resjsonPath) { 135 | Write-Verbose "Loading resource strings from: $resjsonPath" 136 | $count = 0 137 | $resjson = Get-Content -LiteralPath $resjsonPath -Encoding UTF8 | Out-String | ConvertFrom-Json 138 | foreach ($member in (Get-Member -Name loc.messages.* -InputObject $resjson -MemberType NoteProperty)) { 139 | if (!($value = $resjson."$($member.Name)")) { 140 | continue 141 | } 142 | 143 | [string]$key = $member.Name.Substring('loc.messages.'.Length) 144 | $script:resourceStrings[$key] = $value 145 | $count++ 146 | } 147 | 148 | Write-Verbose "Loaded $count strings." 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /hugo-extension-step-by-step.md: -------------------------------------------------------------------------------- 1 | # Using the Hugo Azure DevOps Extension Step-by-Step 2 | This guide assumes that you are already familiar with [Azure DevOps](https://dev.azure.com/) and you have successfully built your Hugo site locally. Test your site to ensure that it functions correctly locally before proceeding with the build automation. If you are going to use the Release steps in this guide you need to prepare an Azure Storage Account with a container for your files. 3 | 4 | ## Create an Azure DevOps Build 5 | 6 | 1. In your Azure DevOps project choose **Build and release**. 7 | 1. Choose **New Build Pipeline** 8 | 1. Select your source repository and default branch and choose **Continue**. 9 | ![Select your repo](images/hugo-ext-build-01.png) 10 | 1. Select the **Empty Job** template. 11 | 1. Rename the build pipeline if you like, then choose the *Get sources** task. Select **Checkout submodules** if your Hugo project uses a theme as a submodule. 12 | 1. Choose the **+** on the Run agent to add a task. 13 | ![Add Hugo build step](images/hugo-ext-build-02.png) 14 | 1. Search for the **Hugo** Azure DevOps extension and choose **Add**. If you have not previously installed the extension you will be taken to the Marketplace where you can add it for free. 15 | ![Add Hugo build step](images/hugo-ext-build-03.png) 16 | 1. Choose the Hugo generate task and review the settings. 17 | * The Source uses **$(Build.SourcesDirectory)** by default 18 | * The Destination can be set to **$(Build.ArtifactStagingDirectory)** (this will save you a copy step later). 19 | * Set your desired Hugo version if necessary. 20 | * Set your Base URL as appropriate for the target host site. This setting enables you to use the same config file for development, test, and production by changing this variable in each build environment. 21 | * Set the additional option settings as appropriate. For example if you are testing content you may want the **Include Drafts** and **Include Future** options. 22 | ![Set Hugo build options](images/hugo-ext-build-04.png) 23 | 1. Add a **Publish Build Artifacts** task below the Hugo build task. 24 | ![Publish Build Artifacts Task](images/hugo-ext-build-05.png) 25 | 1. Configure the Publish task as follows: 26 | * Set the **Path to publish** to **$(Build.ArtifactStagingDirectory)**. This is the contents of the _public_ directory that you see when building your site locally. 27 | * Set the **Artifact name** to **public**. (This step is optional, you can set it to anything you like but I think calling it "public" makes it obvious what it is you are publishing.) 28 | * Leave the **Artifact publish location** set to **Visual Studio Team Services/TFS**. 29 | ![Configure the Publish Build Artifacts Task](images/hugo-ext-build-06.png) 30 | 1. Choose **Save \& Queue** to save and test your work. 31 | 32 | ## Testing the Build 33 | Once the build completes you should see the results in the details of the job history. you should see the same number of files and pages built as you do when performing a local Hugo build. 34 | ![Build log](images/hugo-ext-build-07.png) 35 | 36 | ## Create an Azure DevOps Release 37 | 38 | Once you are certain that the Build is working correctly you can create a Release that uses the **Public** artifact from the Build. The following steps use Azure CLI commands to clear the destination directory in an Azure Storage account and then copy the build files to the storage location. This method can be improved and optimized, but demonstrates one method for using the build artifact in a release. 39 | 40 | >Huge props to Carl-Hugo Marcotte for his blog series [How to deploy and host a Jekyll website in Azure blob storage using a Azure DevOps continuous deployment pipeline](https://www.forevolve.com/en/articles/2018/07/10/how-to-deploy-and-host-a-jekyll-website-in-azure-blob-storage-using-a-vsts-continuous-deployment-pipeline-part-1/) even though it's written for Jekyll it was perfect for this Hugo application. 41 | 42 | 1. Choose **Releases** and choose **+ Add** to create a new release pipeline. Choose **Empty job**. 43 | ![Add build artifact](images/hugo-ext-build-08.png) 44 | 1. In the **Add an artifact** blade choose the artifact from the Build process you created and named above. You should see a confirmation at the bottom of the blade indicating that **Public** is the name of your published build artifact. Click **Add**. 45 | ![Add artifact](images/hugo-ext-build-09.png) 46 | 1. In stage 1, choose the link to view the Stage. 47 | ![View the empty stage](images/hugo-ext-build-10.png) 48 | 1. Click the **+** and search for **CLI**. Add two **Azure CLI** tasks to the run agent. 49 | ![The Azure CLI task](images/hugo-ext-build-11.png) 50 | 1. Choose the first **Azure CLI** task. Configure it as follows: 51 | * Change the **Display name** to **Delete Old Files**. 52 | * Change the **Script location** to **Inline script**. 53 | * Change the **Inline script** to 54 | ```` powershell 55 | az storage blob delete-batch --source $(containerName) --account-name $(storageAccount) --output table 56 | ```` 57 | * Set the **Working directory** to **$(System.DefaultWorkingDirectory)/what-ever-your-build-is** (Use the **...** selector to be sure you get this right.) 58 | 1. Choose the second **Azure CLI** task and configure the settings as follows: 59 | * Change the **Display name** to **Upload new files**. 60 | * Change the **Script location** to **Inline script**. 61 | * Change the **Inline script** to 62 | ```` powershell 63 | az storage blob upload-batch --source $(artifactName) --destination $(containerName) --account-name $(storageAccount) --output table --no-progress 64 | ```` 65 | * Set the **Working directory** to **$(System.DefaultWorkingDirectory)/what-ever-your-build-is** (Use the **...** selector to be sure you get this right.) 66 | 1. Click **Save**. 67 | 1. Choose the **Variables** tab and add the three pipeline variables for the _az storage_ commands. 68 | * **storageAccount** - Your Azure Storage Account name. 69 | * **containerName** - The BLOB container in the storage account. 70 | * **artifactName** - The artifact from the Build pipeline. In this demo that is _public_. 71 | ![Release pipeline variables](images/hugo-ext-build-12.png) 72 | 1. Choose **Save**. 73 | 1. Choose **All pipelines** to return to the main page. On the **Overview** tab choose **Releases | Create a release**. 74 | ![Create a release](images/hugo-ext-build-13.png) 75 | 1. In the **Create a new release** dialog, choose **Create**. 76 | 1. Click the release creation message to view the release in real time. 77 | ![Release confirmation message](images/hugo-ext-build-14.png) 78 | 1. You should see your release proceed through the steps you created for deletion and copying the new files. 79 | ![Release in process](images/hugo-ext-build-15.png) 80 | 1. When the release completes you should see a success message. 81 | ![Release successful](images/hugo-ext-build-16.png) 82 | -------------------------------------------------------------------------------- /hugo-task/ps_modules/VstsTaskSdk/VstsTaskSdk.psm1: -------------------------------------------------------------------------------- 1 | [CmdletBinding()] 2 | param( 3 | [ValidateNotNull()] 4 | [Parameter()] 5 | [hashtable]$ModuleParameters = @{ }) 6 | 7 | if ($host.Name -ne 'ConsoleHost') { 8 | Write-Warning "VstsTaskSdk is designed for use with powershell.exe (ConsoleHost). Output may be different when used with other hosts." 9 | } 10 | 11 | # Private module variables. 12 | [bool]$script:nonInteractive = "$($ModuleParameters['NonInteractive'])" -eq 'true' 13 | Write-Verbose "NonInteractive: $script:nonInteractive" 14 | 15 | # VstsTaskSdk.dll contains the TerminationException and NativeMethods for handle long path 16 | # We used to do inline C# in this powershell module 17 | # However when csc compile the inline C#, it will hit process env block size limit since it's not use unicode to encode env 18 | # To solve the env block size problem, we choose to put all inline C# into an assembly VstsTaskSdk.dll, signing it, package with the PS modules. 19 | Write-Verbose "Loading compiled helper $PSScriptRoot\VstsTaskSdk.dll." 20 | Add-Type -LiteralPath $PSScriptRoot\VstsTaskSdk.dll 21 | 22 | # Import/export functions. 23 | . "$PSScriptRoot\FindFunctions.ps1" 24 | . "$PSScriptRoot\InputFunctions.ps1" 25 | . "$PSScriptRoot\LegacyFindFunctions.ps1" 26 | . "$PSScriptRoot\LocalizationFunctions.ps1" 27 | . "$PSScriptRoot\LoggingCommandFunctions.ps1" 28 | . "$PSScriptRoot\LongPathFunctions.ps1" 29 | . "$PSScriptRoot\ServerOMFunctions.ps1" 30 | . "$PSScriptRoot\ToolFunctions.ps1" 31 | . "$PSScriptRoot\TraceFunctions.ps1" 32 | . "$PSScriptRoot\OutFunctions.ps1" # Load the out functions after all of the other functions are loaded. 33 | Export-ModuleMember -Function @( 34 | # Find functions. 35 | 'Find-Match' 36 | 'New-FindOptions' 37 | 'New-MatchOptions' 38 | 'Select-Match' 39 | # Input functions. 40 | 'Get-Endpoint' 41 | 'Get-Input' 42 | 'Get-TaskVariable' 43 | 'Get-TaskVariableInfo' 44 | 'Set-TaskVariable' 45 | # Legacy find functions. 46 | 'Find-Files' 47 | # Localization functions. 48 | 'Get-LocString' 49 | 'Import-LocStrings' 50 | # Logging command functions. 51 | 'Write-AddAttachment' 52 | 'Write-AddBuildTag' 53 | 'Write-AssociateArtifact' 54 | 'Write-LogDetail' 55 | 'Write-SetProgress' 56 | 'Write-SetResult' 57 | 'Write-SetSecret' 58 | 'Write-SetVariable' 59 | 'Write-TaskDebug' 60 | 'Write-TaskError' 61 | 'Write-TaskVerbose' 62 | 'Write-TaskWarning' 63 | 'Write-UpdateBuildNumber' 64 | 'Write-UploadArtifact' 65 | 'Write-UploadBuildLog' 66 | # Out functions. 67 | 'Out-Default' 68 | # Server OM functions. 69 | 'Get-AssemblyReference' 70 | 'Get-TfsClientCredentials' 71 | 'Get-TfsService' 72 | 'Get-VssCredentials' 73 | 'Get-VssHttpClient' 74 | # Tool functions. 75 | 'Assert-Agent' 76 | 'Assert-Path' 77 | 'Invoke-Tool' 78 | # Trace functions. 79 | 'Trace-EnteringInvocation' 80 | 'Trace-LeavingInvocation' 81 | 'Trace-Path' 82 | # Proxy functions 83 | 'Get-WebProxy' 84 | ) 85 | 86 | # Override Out-Default globally. 87 | $null = New-Item -Force -Path "function:\global:Out-Default" -Value (Get-Command -CommandType Function -Name Out-Default -ListImported) 88 | New-Alias -Name Out-Default -Value "global:Out-Default" -Scope global 89 | 90 | # Perform some initialization in a script block to enable merging the pipelines. 91 | $scriptText = @" 92 | # Load the SDK resource strings. 93 | Import-LocStrings "$PSScriptRoot\lib.json" 94 | 95 | # Load the module that contains ConvertTo-SecureString. 96 | if (!(Get-Module -Name Microsoft.PowerShell.Security)) { 97 | Write-Verbose "Importing the module 'Microsoft.PowerShell.Security'." 98 | Import-Module -Name Microsoft.PowerShell.Security 2>&1 | 99 | ForEach-Object { 100 | if (`$_ -is [System.Management.Automation.ErrorRecord]) { 101 | Write-Verbose `$_.Exception.Message 102 | } else { 103 | ,`$_ 104 | } 105 | } 106 | } 107 | "@ 108 | . ([scriptblock]::Create($scriptText)) 2>&1 3>&1 4>&1 5>&1 | Out-Default 109 | 110 | # Create Invoke-VstsTaskScript in a special way so it is not bound to the module. 111 | # Otherwise calling the task script block would run within the module context. 112 | # 113 | # An alternative way to solve the problem is to close the script block (i.e. closure). 114 | # However, that introduces a different problem. Closed script blocks are created within 115 | # a dynamic module. Each module gets it's own session state separate from the global 116 | # session state. When running in a regular script context, Import-Module calls import 117 | # the target module into the global session state. When running in a module context, 118 | # Import-Module calls import the target module into the caller module's session state. 119 | # 120 | # The goal of a task may include executing ad-hoc scripts. Therefore, task scripts 121 | # should run in regular script context. The end user specifying an ad-hoc script expects 122 | # the module import rules to be consistent with the default behavior (i.e. imported 123 | # into the global session state). 124 | $null = New-Item -Force -Path "function:\global:Invoke-VstsTaskScript" -Value ([scriptblock]::Create(@' 125 | [CmdletBinding()] 126 | param( 127 | [Parameter(Mandatory = $true)] 128 | [scriptblock]$ScriptBlock) 129 | 130 | try { 131 | $global:ErrorActionPreference = 'Stop' 132 | 133 | # Initialize the environment. 134 | $vstsModule = Get-Module -Name VstsTaskSdk 135 | Write-Verbose "$($vstsModule.Name) $($vstsModule.Version) commit $($vstsModule.PrivateData.PSData.CommitHash)" 4>&1 | Out-Default 136 | & $vstsModule Initialize-Inputs 4>&1 | Out-Default 137 | 138 | # Remove the local variable before calling the user's script. 139 | Remove-Variable -Name vstsModule 140 | 141 | # Call the user's script. 142 | $ScriptBlock | 143 | ForEach-Object { 144 | # Remove the scriptblock variable before calling it. 145 | Remove-Variable -Name ScriptBlock 146 | & $_ 2>&1 3>&1 4>&1 5>&1 | Out-Default 147 | } 148 | } catch [VstsTaskSdk.TerminationException] { 149 | # Special internal exception type to control the flow. Not currently intended 150 | # for public usage and subject to change. 151 | $global:__vstsNoOverrideVerbose = '' 152 | Write-Verbose "Task script terminated." 4>&1 | Out-Default 153 | } catch { 154 | $global:__vstsNoOverrideVerbose = '' 155 | Write-Verbose "Caught exception from task script." 4>&1 | Out-Default 156 | $_ | Out-Default 157 | Write-Host "##vso[task.complete result=Failed]" 158 | } 159 | '@)) 160 | -------------------------------------------------------------------------------- /hugo-taskV2/src/HugoTask.ts: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import * as toolLib from 'azure-pipelines-tool-lib/tool'; 4 | import tl = require("azure-pipelines-task-lib/task"); 5 | import tr = require("azure-pipelines-task-lib/toolrunner"); 6 | import * as fs from 'fs'; 7 | import * as os from 'os'; 8 | import * as path from 'path'; 9 | import * as util from 'util'; 10 | 11 | const osPlat: string = os.platform(); 12 | const osArch: string = os.arch(); 13 | const cacheKey = 'hugo'; 14 | 15 | async function run() { 16 | try { 17 | tl.setResourcePath(path.join(__dirname, "task.json")); 18 | 19 | // download version 20 | const hugoVersion: string = tl.getInput("hugoVersion", false); 21 | const extendedVersion: boolean = tl.getBoolInput('extendedVersion', false); 22 | 23 | await getHugo(hugoVersion, extendedVersion); 24 | 25 | const source: string = tl.getPathInput('source', true, false); 26 | const destination: string = tl.getPathInput('destination', true, false); 27 | const baseURL: string = tl.getInput('baseURL', false); 28 | const buildDrafts: boolean = tl.getBoolInput('buildDrafts', false); 29 | const buildExpired: boolean = tl.getBoolInput('buildExpired', false); 30 | const buildFuture: boolean = tl.getBoolInput('buildFuture', false); 31 | const additionalArgs: string = tl.getInput('additionalArgs', false); 32 | 33 | const hugoPath = tl.which(cacheKey, true); 34 | const hugo: tr.ToolRunner = tl.tool(hugoPath); 35 | 36 | hugo.argIf(source, ['--source',source]); 37 | hugo.argIf(destination, ['--destination',destination]); 38 | hugo.argIf(baseURL, ['--baseURL ',baseURL]); 39 | hugo.argIf(buildDrafts, '--buildDrafts'); 40 | hugo.argIf(buildExpired, '--buildExpired'); 41 | hugo.argIf(buildFuture, '--buildFuture'); 42 | 43 | // implicit flags 44 | hugo.line(' --i18n-warnings --path-warnings --verbose'); 45 | hugo.line(additionalArgs); 46 | 47 | await hugo.exec(); 48 | 49 | } catch (error) { 50 | tl.setResult(tl.TaskResult.Failed, error); 51 | } 52 | } 53 | 54 | async function getHugo(version: string, extendedVersion: boolean): Promise { 55 | const latest = 'latest'; 56 | if (version && version !== latest) { 57 | version = sanitizeVersionString(version); 58 | } 59 | if (version == latest) { 60 | version = await getLatestGitHubRelease(); 61 | } 62 | 63 | // check cache 64 | let toolPath: string; 65 | toolPath = toolLib.findLocalTool(cacheKey, version); 66 | 67 | if (!toolPath) { 68 | // download, extract, cache 69 | toolPath = await acquireHugo(version, extendedVersion); 70 | tl.debug("Hugo tool is cached under " + toolPath); 71 | } 72 | 73 | // prepend the tools path. instructs the agent to prepend for future tasks 74 | toolLib.prependPath(toolPath); 75 | } 76 | 77 | const defaultHugoVersion = '0.91.2'; 78 | 79 | async function getLatestGitHubRelease(): Promise { 80 | const latestReleaseUrl = 'https://api.github.com/repos/gohugoio/hugo/releases/latest?draft=false'; 81 | let latestVersion = defaultHugoVersion; 82 | // TODO At most once a day? where to cache whether run today? 83 | try { 84 | const downloadPath = await toolLib.downloadTool(latestReleaseUrl); 85 | const response = JSON.parse(fs.readFileSync(downloadPath, 'utf8').toString().trim()); 86 | if (response.tag_name) { 87 | latestVersion = response.tag_name.substring(1); // skip 'v' prefix 88 | } 89 | } catch (error) { 90 | tl.warning(util.format('Error while fetching Latest version from %s, assuming %s: %s', latestReleaseUrl, latestVersion, error)); 91 | } 92 | return latestVersion; 93 | } 94 | 95 | async function acquireHugo(version: string, extendedVersion: boolean): Promise { 96 | // 97 | // Download - a tool installer intimately knows how to get the tool (and construct urls) 98 | // 99 | const fileName: string = getFileName(version, extendedVersion); 100 | const downloadUrl: string = getDownloadUrl(version, fileName); 101 | let downloadPath: string = null; 102 | try { 103 | downloadPath = await toolLib.downloadTool(downloadUrl); 104 | // TODO check SHA 105 | } catch (error) { 106 | tl.debug(error); 107 | 108 | // cannot localize the string here because to localize we need to set the resource file. 109 | // which can be set only once. azure-pipelines-tool-lib/tool, is already setting it to different file. 110 | // So left with no option but to hardcode the string. Other tasks are doing the same. 111 | throw (util.format("Failed to download version %s. Please verify that the version is valid and resolve any other issues. %s", version, error)); 112 | } 113 | 114 | //make sure agent version is latest then 2.115.0 115 | tl.assertAgent('2.105.7'); 116 | 117 | // Extract 118 | let extPath: string; 119 | extPath = tl.getVariable('Agent.TempDirectory'); 120 | if (!extPath) { 121 | throw new Error("Expected Agent.TempDirectory to be set"); 122 | } 123 | 124 | if (osPlat == 'win32') { 125 | extPath = await toolLib.extractZip(downloadPath); 126 | } 127 | else { 128 | extPath = await toolLib.extractTar(downloadPath); 129 | } 130 | 131 | // Install into the local tool cache - node extracts with a root folder that matches the fileName downloaded 132 | return await toolLib.cacheDir(extPath, cacheKey, version); 133 | } 134 | 135 | function getFileName(version: string, extendedVersion: boolean): string { 136 | let platform: string = osPlat; 137 | // 'aix', 'darwin', 'freebsd', 'linux', 'openbsd', 'sunos', and 'win32'. 138 | switch (osPlat) { 139 | case 'win32': 140 | platform = 'windows'; 141 | break; 142 | case 'darwin': 143 | platform = 'macOS'; 144 | break; 145 | } 146 | let arch: string = osArch; 147 | // 'arm', 'arm64', 'ia32', 'mips', 'mipsel', 'ppc', 'ppc64', 's390', 's390x', 'x32', and 'x64'. 148 | switch (osArch) { 149 | case 'x64': 150 | arch = '64bit'; 151 | break; 152 | case 'ia32': 153 | case 'x32': 154 | arch = '32bit'; 155 | break; 156 | } 157 | const ext: string = osPlat == "win32" ? "zip" : "tar.gz"; 158 | const filename: string = extendedVersion 159 | ? util.format("hugo_extended_%s_%s-%s.%s", version, platform, arch, ext) 160 | : util.format("hugo_%s_%s-%s.%s", version, platform, arch, ext); 161 | return filename; 162 | } 163 | 164 | function getDownloadUrl(version: string, filename: string): string { 165 | return util.format("https://github.com/gohugoio/hugo/releases/download/v%s/%s", version, filename); 166 | } 167 | 168 | // handle user input scenerios 169 | function sanitizeVersionString(inputVersion: string) : string{ 170 | const version = toolLib.cleanVersion(inputVersion); 171 | if(!version) { 172 | throw new Error(tl.loc("NotAValidSemverVersion")); 173 | } 174 | 175 | return version; 176 | } 177 | 178 | run(); -------------------------------------------------------------------------------- /hugo-task/ps_modules/VstsTaskSdk/LongPathFunctions.ps1: -------------------------------------------------------------------------------- 1 | ######################################## 2 | # Private functions. 3 | ######################################## 4 | function ConvertFrom-LongFormPath { 5 | [CmdletBinding()] 6 | param([string]$Path) 7 | 8 | if ($Path) { 9 | if ($Path.StartsWith('\\?\UNC')) { 10 | # E.g. \\?\UNC\server\share -> \\server\share 11 | return $Path.Substring(1, '\?\UNC'.Length) 12 | } elseif ($Path.StartsWith('\\?\')) { 13 | # E.g. \\?\C:\directory -> C:\directory 14 | return $Path.Substring('\\?\'.Length) 15 | } 16 | } 17 | 18 | return $Path 19 | } 20 | function ConvertTo-LongFormPath { 21 | [CmdletBinding()] 22 | param( 23 | [Parameter(Mandatory = $true)] 24 | [string]$Path) 25 | 26 | [string]$longFormPath = Get-FullNormalizedPath -Path $Path 27 | if ($longFormPath -and !$longFormPath.StartsWith('\\?')) { 28 | if ($longFormPath.StartsWith('\\')) { 29 | # E.g. \\server\share -> \\?\UNC\server\share 30 | return "\\?\UNC$($longFormPath.Substring(1))" 31 | } else { 32 | # E.g. C:\directory -> \\?\C:\directory 33 | return "\\?\$longFormPath" 34 | } 35 | } 36 | 37 | return $longFormPath 38 | } 39 | 40 | # TODO: ADD A SWITCH TO EXCLUDE FILES, A SWITCH TO EXCLUDE DIRECTORIES, AND A SWITCH NOT TO FOLLOW REPARSE POINTS. 41 | function Get-DirectoryChildItem { 42 | [CmdletBinding()] 43 | param( 44 | [string]$Path, 45 | [ValidateNotNullOrEmpty()] 46 | [Parameter()] 47 | [string]$Filter = "*", 48 | [switch]$Force, 49 | [VstsTaskSdk.FS.FindFlags]$Flags = [VstsTaskSdk.FS.FindFlags]::LargeFetch, 50 | [VstsTaskSdk.FS.FindInfoLevel]$InfoLevel = [VstsTaskSdk.FS.FindInfoLevel]::Basic, 51 | [switch]$Recurse) 52 | 53 | $stackOfDirectoryQueues = New-Object System.Collections.Stack 54 | while ($true) { 55 | $directoryQueue = New-Object System.Collections.Queue 56 | $fileQueue = New-Object System.Collections.Queue 57 | $findData = New-Object VstsTaskSdk.FS.FindData 58 | $longFormPath = (ConvertTo-LongFormPath $Path) 59 | $handle = $null 60 | try { 61 | $handle = [VstsTaskSdk.FS.NativeMethods]::FindFirstFileEx( 62 | [System.IO.Path]::Combine($longFormPath, $Filter), 63 | $InfoLevel, 64 | $findData, 65 | [VstsTaskSdk.FS.FindSearchOps]::NameMatch, 66 | [System.IntPtr]::Zero, 67 | $Flags) 68 | if (!$handle.IsInvalid) { 69 | while ($true) { 70 | if ($findData.fileName -notin '.', '..') { 71 | $attributes = [VstsTaskSdk.FS.Attributes]$findData.fileAttributes 72 | # If the item is hidden, check if $Force is specified. 73 | if ($Force -or !$attributes.HasFlag([VstsTaskSdk.FS.Attributes]::Hidden)) { 74 | # Create the item. 75 | $item = New-Object -TypeName psobject -Property @{ 76 | 'Attributes' = $attributes 77 | 'FullName' = (ConvertFrom-LongFormPath -Path ([System.IO.Path]::Combine($Path, $findData.fileName))) 78 | 'Name' = $findData.fileName 79 | } 80 | # Output directories immediately. 81 | if ($item.Attributes.HasFlag([VstsTaskSdk.FS.Attributes]::Directory)) { 82 | $item 83 | # Append to the directory queue if recursive and default filter. 84 | if ($Recurse -and $Filter -eq '*') { 85 | $directoryQueue.Enqueue($item) 86 | } 87 | } else { 88 | # Hold the files until all directories have been output. 89 | $fileQueue.Enqueue($item) 90 | } 91 | } 92 | } 93 | 94 | if (!([VstsTaskSdk.FS.NativeMethods]::FindNextFile($handle, $findData))) { break } 95 | 96 | if ($handle.IsInvalid) { 97 | throw (New-Object -TypeName System.ComponentModel.Win32Exception -ArgumentList @( 98 | [System.Runtime.InteropServices.Marshal]::GetLastWin32Error() 99 | Get-LocString -Key PSLIB_EnumeratingSubdirectoriesFailedForPath0 -ArgumentList $Path 100 | )) 101 | } 102 | } 103 | } 104 | } finally { 105 | if ($handle -ne $null) { $handle.Dispose() } 106 | } 107 | 108 | # If recursive and non-default filter, queue child directories. 109 | if ($Recurse -and $Filter -ne '*') { 110 | $findData = New-Object VstsTaskSdk.FS.FindData 111 | $handle = $null 112 | try { 113 | $handle = [VstsTaskSdk.FS.NativeMethods]::FindFirstFileEx( 114 | [System.IO.Path]::Combine($longFormPath, '*'), 115 | [VstsTaskSdk.FS.FindInfoLevel]::Basic, 116 | $findData, 117 | [VstsTaskSdk.FS.FindSearchOps]::NameMatch, 118 | [System.IntPtr]::Zero, 119 | $Flags) 120 | if (!$handle.IsInvalid) { 121 | while ($true) { 122 | if ($findData.fileName -notin '.', '..') { 123 | $attributes = [VstsTaskSdk.FS.Attributes]$findData.fileAttributes 124 | # If the item is hidden, check if $Force is specified. 125 | if ($Force -or !$attributes.HasFlag([VstsTaskSdk.FS.Attributes]::Hidden)) { 126 | # Collect directories only. 127 | if ($attributes.HasFlag([VstsTaskSdk.FS.Attributes]::Directory)) { 128 | # Create the item. 129 | $item = New-Object -TypeName psobject -Property @{ 130 | 'Attributes' = $attributes 131 | 'FullName' = (ConvertFrom-LongFormPath -Path ([System.IO.Path]::Combine($Path, $findData.fileName))) 132 | 'Name' = $findData.fileName 133 | } 134 | $directoryQueue.Enqueue($item) 135 | } 136 | } 137 | } 138 | 139 | if (!([VstsTaskSdk.FS.NativeMethods]::FindNextFile($handle, $findData))) { break } 140 | 141 | if ($handle.IsInvalid) { 142 | throw (New-Object -TypeName System.ComponentModel.Win32Exception -ArgumentList @( 143 | [System.Runtime.InteropServices.Marshal]::GetLastWin32Error() 144 | Get-LocString -Key PSLIB_EnumeratingSubdirectoriesFailedForPath0 -ArgumentList $Path 145 | )) 146 | } 147 | } 148 | } 149 | } finally { 150 | if ($handle -ne $null) { $handle.Dispose() } 151 | } 152 | } 153 | 154 | # Output the files. 155 | $fileQueue 156 | 157 | # Push the directory queue onto the stack if any directories were found. 158 | if ($directoryQueue.Count) { $stackOfDirectoryQueues.Push($directoryQueue) } 159 | 160 | # Break out of the loop if no more directory queues to process. 161 | if (!$stackOfDirectoryQueues.Count) { break } 162 | 163 | # Get the next path. 164 | $directoryQueue = $stackOfDirectoryQueues.Peek() 165 | $Path = $directoryQueue.Dequeue().FullName 166 | 167 | # Pop the directory queue if it's empty. 168 | if (!$directoryQueue.Count) { $null = $stackOfDirectoryQueues.Pop() } 169 | } 170 | } 171 | 172 | function Get-FullNormalizedPath { 173 | [CmdletBinding()] 174 | param( 175 | [Parameter(Mandatory = $true)] 176 | [string]$Path) 177 | 178 | [string]$outPath = $Path 179 | [uint32]$bufferSize = [VstsTaskSdk.FS.NativeMethods]::GetFullPathName($Path, 0, $null, $null) 180 | [int]$lastWin32Error = [System.Runtime.InteropServices.Marshal]::GetLastWin32Error() 181 | if ($bufferSize -gt 0) { 182 | $absolutePath = New-Object System.Text.StringBuilder([int]$bufferSize) 183 | [uint32]$length = [VstsTaskSdk.FS.NativeMethods]::GetFullPathName($Path, $bufferSize, $absolutePath, $null) 184 | $lastWin32Error = [System.Runtime.InteropServices.Marshal]::GetLastWin32Error() 185 | if ($length -gt 0) { 186 | $outPath = $absolutePath.ToString() 187 | } else { 188 | throw (New-Object -TypeName System.ComponentModel.Win32Exception -ArgumentList @( 189 | $lastWin32Error 190 | Get-LocString -Key PSLIB_PathLengthNotReturnedFor0 -ArgumentList $Path 191 | )) 192 | } 193 | } else { 194 | throw (New-Object -TypeName System.ComponentModel.Win32Exception -ArgumentList @( 195 | $lastWin32Error 196 | Get-LocString -Key PSLIB_PathLengthNotReturnedFor0 -ArgumentList $Path 197 | )) 198 | } 199 | 200 | if ($outPath.EndsWith('\') -and !$outPath.EndsWith(':\')) { 201 | $outPath = $outPath.TrimEnd('\') 202 | } 203 | 204 | $outPath 205 | } -------------------------------------------------------------------------------- /hugo-task/ps_modules/VstsTaskSdk/LegacyFindFunctions.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Finds files or directories. 4 | 5 | .DESCRIPTION 6 | Finds files or directories using advanced pattern matching. 7 | 8 | .PARAMETER LiteralDirectory 9 | Directory to search. 10 | 11 | .PARAMETER LegacyPattern 12 | Proprietary pattern format. The LiteralDirectory parameter is used to root any unrooted patterns. 13 | 14 | Separate multiple patterns using ";". Escape actual ";" in the path by using ";;". 15 | "?" indicates a wildcard that represents any single character within a path segment. 16 | "*" indicates a wildcard that represents zero or more characters within a path segment. 17 | "**" as the entire path segment indicates a recursive search. 18 | "**" within a path segment indicates a recursive intersegment wildcard. 19 | "+:" (can be omitted) indicates an include pattern. 20 | "-:" indicates an exclude pattern. 21 | 22 | The result is from the command is a union of all the matches from the include patterns, minus the matches from the exclude patterns. 23 | 24 | .PARAMETER IncludeFiles 25 | Indicates whether to include files in the results. 26 | 27 | If neither IncludeFiles or IncludeDirectories is set, then IncludeFiles is assumed. 28 | 29 | .PARAMETER IncludeDirectories 30 | Indicates whether to include directories in the results. 31 | 32 | If neither IncludeFiles or IncludeDirectories is set, then IncludeFiles is assumed. 33 | 34 | .PARAMETER Force 35 | Indicates whether to include hidden items. 36 | 37 | .EXAMPLE 38 | Find-VstsFiles -LegacyPattern "C:\Directory\Is?Match.txt" 39 | 40 | Given: 41 | C:\Directory\Is1Match.txt 42 | C:\Directory\Is2Match.txt 43 | C:\Directory\IsNotMatch.txt 44 | 45 | Returns: 46 | C:\Directory\Is1Match.txt 47 | C:\Directory\Is2Match.txt 48 | 49 | .EXAMPLE 50 | Find-VstsFiles -LegacyPattern "C:\Directory\Is*Match.txt" 51 | 52 | Given: 53 | C:\Directory\IsOneMatch.txt 54 | C:\Directory\IsTwoMatch.txt 55 | C:\Directory\NonMatch.txt 56 | 57 | Returns: 58 | C:\Directory\IsOneMatch.txt 59 | C:\Directory\IsTwoMatch.txt 60 | 61 | .EXAMPLE 62 | Find-VstsFiles -LegacyPattern "C:\Directory\**\Match.txt" 63 | 64 | Given: 65 | C:\Directory\Match.txt 66 | C:\Directory\NotAMatch.txt 67 | C:\Directory\SubDir\Match.txt 68 | C:\Directory\SubDir\SubSubDir\Match.txt 69 | 70 | Returns: 71 | C:\Directory\Match.txt 72 | C:\Directory\SubDir\Match.txt 73 | C:\Directory\SubDir\SubSubDir\Match.txt 74 | 75 | .EXAMPLE 76 | Find-VstsFiles -LegacyPattern "C:\Directory\**" 77 | 78 | Given: 79 | C:\Directory\One.txt 80 | C:\Directory\SubDir\Two.txt 81 | C:\Directory\SubDir\SubSubDir\Three.txt 82 | 83 | Returns: 84 | C:\Directory\One.txt 85 | C:\Directory\SubDir\Two.txt 86 | C:\Directory\SubDir\SubSubDir\Three.txt 87 | 88 | .EXAMPLE 89 | Find-VstsFiles -LegacyPattern "C:\Directory\Sub**Match.txt" 90 | 91 | Given: 92 | C:\Directory\IsNotAMatch.txt 93 | C:\Directory\SubDir\IsAMatch.txt 94 | C:\Directory\SubDir\IsNot.txt 95 | C:\Directory\SubDir\SubSubDir\IsAMatch.txt 96 | C:\Directory\SubDir\SubSubDir\IsNot.txt 97 | 98 | Returns: 99 | C:\Directory\SubDir\IsAMatch.txt 100 | C:\Directory\SubDir\SubSubDir\IsAMatch.txt 101 | #> 102 | function Find-Files { 103 | [CmdletBinding()] 104 | param( 105 | [ValidateNotNullOrEmpty()] 106 | [Parameter()] 107 | [string]$LiteralDirectory, 108 | [Parameter(Mandatory = $true)] 109 | [string]$LegacyPattern, 110 | [switch]$IncludeFiles, 111 | [switch]$IncludeDirectories, 112 | [switch]$Force) 113 | 114 | # Note, due to subtle implementation details of Get-PathPrefix/Get-PathIterator, 115 | # this function does not appear to be able to search the root of a drive and other 116 | # cases where Path.GetDirectoryName() returns empty. More details in Get-PathPrefix. 117 | 118 | Trace-EnteringInvocation $MyInvocation 119 | if (!$IncludeFiles -and !$IncludeDirectories) { 120 | $IncludeFiles = $true 121 | } 122 | 123 | $includePatterns = New-Object System.Collections.Generic.List[string] 124 | $excludePatterns = New-Object System.Collections.Generic.List[System.Text.RegularExpressions.Regex] 125 | $LegacyPattern = $LegacyPattern.Replace(';;', "`0") 126 | foreach ($pattern in $LegacyPattern.Split(';', [System.StringSplitOptions]::RemoveEmptyEntries)) { 127 | $pattern = $pattern.Replace("`0", ';') 128 | $isIncludePattern = Test-IsIncludePattern -Pattern ([ref]$pattern) 129 | if ($LiteralDirectory -and !([System.IO.Path]::IsPathRooted($pattern))) { 130 | # Use the root directory provided to make the pattern a rooted path. 131 | $pattern = [System.IO.Path]::Combine($LiteralDirectory, $pattern) 132 | } 133 | 134 | # Validate pattern does not end with a \. 135 | if ($pattern[$pattern.Length - 1] -eq [System.IO.Path]::DirectorySeparatorChar) { 136 | throw (Get-LocString -Key PSLIB_InvalidPattern0 -ArgumentList $pattern) 137 | } 138 | 139 | if ($isIncludePattern) { 140 | $includePatterns.Add($pattern) 141 | } else { 142 | $excludePatterns.Add((Convert-PatternToRegex -Pattern $pattern)) 143 | } 144 | } 145 | 146 | $count = 0 147 | foreach ($path in (Get-MatchingItems -IncludePatterns $includePatterns -ExcludePatterns $excludePatterns -IncludeFiles:$IncludeFiles -IncludeDirectories:$IncludeDirectories -Force:$Force)) { 148 | $count++ 149 | $path 150 | } 151 | 152 | Write-Verbose "Total found: $count" 153 | Trace-LeavingInvocation $MyInvocation 154 | } 155 | 156 | ######################################## 157 | # Private functions. 158 | ######################################## 159 | function Convert-PatternToRegex { 160 | [CmdletBinding()] 161 | param([string]$Pattern) 162 | 163 | $Pattern = [regex]::Escape($Pattern.Replace('\', '/')). # Normalize separators and regex escape. 164 | Replace('/\*\*/', '((/.+/)|(/))'). # Replace directory globstar. 165 | Replace('\*\*', '.*'). # Replace remaining globstars with a wildcard that can span directory separators. 166 | Replace('\*', '[^/]*'). # Replace asterisks with a wildcard that cannot span directory separators. 167 | # bug: should be '[^/]' instead of '.' 168 | Replace('\?', '.') # Replace single character wildcards. 169 | New-Object regex -ArgumentList "^$Pattern`$", ([System.Text.RegularExpressions.RegexOptions]::IgnoreCase) 170 | } 171 | 172 | function Get-FileNameFilter { 173 | [CmdletBinding()] 174 | param([string]$Pattern) 175 | 176 | $index = $Pattern.LastIndexOf('\') 177 | if ($index -eq -1 -or # Pattern does not contain a backslash. 178 | !($Pattern = $Pattern.Substring($index + 1)) -or # Pattern ends in a backslash. 179 | $Pattern.Contains('**')) # Last segment contains an inter-segment wildcard. 180 | { 181 | return '*' 182 | } 183 | 184 | # bug? is this supposed to do substring? 185 | return $Pattern 186 | } 187 | 188 | function Get-MatchingItems { 189 | [CmdletBinding()] 190 | param( 191 | [System.Collections.Generic.List[string]]$IncludePatterns, 192 | [System.Collections.Generic.List[regex]]$ExcludePatterns, 193 | [switch]$IncludeFiles, 194 | [switch]$IncludeDirectories, 195 | [switch]$Force) 196 | 197 | Trace-EnteringInvocation $MyInvocation 198 | $allFiles = New-Object System.Collections.Generic.HashSet[string] 199 | foreach ($pattern in $IncludePatterns) { 200 | $pathPrefix = Get-PathPrefix -Pattern $pattern 201 | $fileNameFilter = Get-FileNameFilter -Pattern $pattern 202 | $patternRegex = Convert-PatternToRegex -Pattern $pattern 203 | # Iterate over the directories and files under the pathPrefix. 204 | Get-PathIterator -Path $pathPrefix -Filter $fileNameFilter -IncludeFiles:$IncludeFiles -IncludeDirectories:$IncludeDirectories -Force:$Force | 205 | ForEach-Object { 206 | # Normalize separators. 207 | $normalizedPath = $_.Replace('\', '/') 208 | # **/times/** will not match C:/fun/times because there isn't a trailing slash. 209 | # So try both if including directories. 210 | $alternatePath = "$normalizedPath/" # potential bug: it looks like this will result in a false 211 | # positive if the item is a regular file and not a directory 212 | 213 | $isMatch = $false 214 | if ($patternRegex.IsMatch($normalizedPath) -or ($IncludeDirectories -and $patternRegex.IsMatch($alternatePath))) { 215 | $isMatch = $true 216 | 217 | # Test whether the path should be excluded. 218 | foreach ($regex in $ExcludePatterns) { 219 | if ($regex.IsMatch($normalizedPath) -or ($IncludeDirectories -and $regex.IsMatch($alternatePath))) { 220 | $isMatch = $false 221 | break 222 | } 223 | } 224 | } 225 | 226 | if ($isMatch) { 227 | $null = $allFiles.Add($_) 228 | } 229 | } 230 | } 231 | 232 | Trace-Path -Path $allFiles -PassThru 233 | Trace-LeavingInvocation $MyInvocation 234 | } 235 | 236 | function Get-PathIterator { 237 | [CmdletBinding()] 238 | param( 239 | [string]$Path, 240 | [string]$Filter, 241 | [switch]$IncludeFiles, 242 | [switch]$IncludeDirectories, 243 | [switch]$Force) 244 | 245 | if (!$Path) { 246 | return 247 | } 248 | 249 | # bug: this returns the dir without verifying whether exists 250 | if ($IncludeDirectories) { 251 | $Path 252 | } 253 | 254 | Get-DirectoryChildItem -Path $Path -Filter $Filter -Force:$Force -Recurse | 255 | ForEach-Object { 256 | if ($_.Attributes.HasFlag([VstsTaskSdk.FS.Attributes]::Directory)) { 257 | if ($IncludeDirectories) { 258 | $_.FullName 259 | } 260 | } elseif ($IncludeFiles) { 261 | $_.FullName 262 | } 263 | } 264 | } 265 | 266 | function Get-PathPrefix { 267 | [CmdletBinding()] 268 | param([string]$Pattern) 269 | 270 | # Note, unable to search root directories is a limitation due to subtleties of this function 271 | # and downstream code in Get-PathIterator that short-circuits when the path prefix is empty. 272 | # This function uses Path.GetDirectoryName() to determine the path prefix, which will yield 273 | # empty in some cases. See the following examples of Path.GetDirectoryName() input => output: 274 | # C:/ => 275 | # C:/hello => C:\ 276 | # C:/hello/ => C:\hello 277 | # C:/hello/world => C:\hello 278 | # C:/hello/world/ => C:\hello\world 279 | # C: => 280 | # C:hello => C: 281 | # C:hello/ => C:hello 282 | # / => 283 | # /hello => \ 284 | # /hello/ => \hello 285 | # //hello => 286 | # //hello/ => 287 | # //hello/world => 288 | # //hello/world/ => \\hello\world 289 | 290 | $index = $Pattern.IndexOfAny([char[]]@('*'[0], '?'[0])) 291 | if ($index -eq -1) { 292 | # If no wildcards are found, return the directory name portion of the path. 293 | # If there is no directory name (file name only in pattern), this will return empty string. 294 | return [System.IO.Path]::GetDirectoryName($Pattern) 295 | } 296 | 297 | [System.IO.Path]::GetDirectoryName($Pattern.Substring(0, $index)) 298 | } 299 | 300 | function Test-IsIncludePattern { 301 | [CmdletBinding()] 302 | param( 303 | [Parameter(Mandatory = $true)] 304 | [ref]$Pattern) 305 | 306 | # Include patterns start with +: or anything except -: 307 | # Exclude patterns start with -: 308 | if ($Pattern.value.StartsWith("+:")) { 309 | # Remove the prefix. 310 | $Pattern.value = $Pattern.value.Substring(2) 311 | $true 312 | } elseif ($Pattern.value.StartsWith("-:")) { 313 | # Remove the prefix. 314 | $Pattern.value = $Pattern.value.Substring(2) 315 | $false 316 | } else { 317 | # No prefix, so leave the string alone. 318 | $true; 319 | } 320 | } 321 | -------------------------------------------------------------------------------- /hugo-task/ps_modules/VstsTaskSdk/InputFunctions.ps1: -------------------------------------------------------------------------------- 1 | # Hash table of known variable info. The formatted env var name is the lookup key. 2 | # 3 | # The purpose of this hash table is to keep track of known variables. The hash table 4 | # needs to be maintained for multiple reasons: 5 | # 1) to distinguish between env vars and job vars 6 | # 2) to distinguish between secret vars and public 7 | # 3) to know the real variable name and not just the formatted env var name. 8 | $script:knownVariables = @{ } 9 | $script:vault = @{ } 10 | 11 | <# 12 | .SYNOPSIS 13 | Gets an endpoint. 14 | 15 | .DESCRIPTION 16 | Gets an endpoint object for the specified endpoint name. The endpoint is returned as an object with three properties: Auth, Data, and Url. 17 | 18 | The Data property requires a 1.97 agent or higher. 19 | 20 | .PARAMETER Require 21 | Writes an error to the error pipeline if the endpoint is not found. 22 | #> 23 | function Get-Endpoint { 24 | [CmdletBinding()] 25 | param( 26 | [Parameter(Mandatory = $true)] 27 | [string]$Name, 28 | [switch]$Require) 29 | 30 | $originalErrorActionPreference = $ErrorActionPreference 31 | try { 32 | $ErrorActionPreference = 'Stop' 33 | 34 | # Get the URL. 35 | $description = Get-LocString -Key PSLIB_EndpointUrl0 -ArgumentList $Name 36 | $key = "ENDPOINT_URL_$Name" 37 | $url = Get-VaultValue -Description $description -Key $key -Require:$Require 38 | 39 | # Get the auth object. 40 | $description = Get-LocString -Key PSLIB_EndpointAuth0 -ArgumentList $Name 41 | $key = "ENDPOINT_AUTH_$Name" 42 | if ($auth = (Get-VaultValue -Description $description -Key $key -Require:$Require)) { 43 | $auth = ConvertFrom-Json -InputObject $auth 44 | } 45 | 46 | # Get the data. 47 | $description = "'$Name' service endpoint data" 48 | $key = "ENDPOINT_DATA_$Name" 49 | if ($data = (Get-VaultValue -Description $description -Key $key)) { 50 | $data = ConvertFrom-Json -InputObject $data 51 | } 52 | 53 | # Return the endpoint. 54 | if ($url -or $auth -or $data) { 55 | New-Object -TypeName psobject -Property @{ 56 | Url = $url 57 | Auth = $auth 58 | Data = $data 59 | } 60 | } 61 | } catch { 62 | $ErrorActionPreference = $originalErrorActionPreference 63 | Write-Error $_ 64 | } 65 | } 66 | 67 | <# 68 | .SYNOPSIS 69 | Gets an input. 70 | 71 | .DESCRIPTION 72 | Gets the value for the specified input name. 73 | 74 | .PARAMETER AsBool 75 | Returns the value as a bool. Returns true if the value converted to a string is "1" or "true" (case insensitive); otherwise false. 76 | 77 | .PARAMETER AsInt 78 | Returns the value as an int. Returns the value converted to an int. Returns 0 if the conversion fails. 79 | 80 | .PARAMETER Default 81 | Default value to use if the input is null or empty. 82 | 83 | .PARAMETER Require 84 | Writes an error to the error pipeline if the input is null or empty. 85 | #> 86 | function Get-Input { 87 | [CmdletBinding(DefaultParameterSetName = 'Require')] 88 | param( 89 | [Parameter(Mandatory = $true)] 90 | [string]$Name, 91 | [Parameter(ParameterSetName = 'Default')] 92 | $Default, 93 | [Parameter(ParameterSetName = 'Require')] 94 | [switch]$Require, 95 | [switch]$AsBool, 96 | [switch]$AsInt) 97 | 98 | # Get the input from the vault. Splat the bound parameters hashtable. Splatting is required 99 | # in order to concisely invoke the correct parameter set. 100 | $null = $PSBoundParameters.Remove('Name') 101 | $description = Get-LocString -Key PSLIB_Input0 -ArgumentList $Name 102 | $key = "INPUT_$($Name.Replace(' ', '_').ToUpperInvariant())" 103 | Get-VaultValue @PSBoundParameters -Description $description -Key $key 104 | } 105 | 106 | <# 107 | .SYNOPSIS 108 | Gets a task variable. 109 | 110 | .DESCRIPTION 111 | Gets the value for the specified task variable. 112 | 113 | .PARAMETER AsBool 114 | Returns the value as a bool. Returns true if the value converted to a string is "1" or "true" (case insensitive); otherwise false. 115 | 116 | .PARAMETER AsInt 117 | Returns the value as an int. Returns the value converted to an int. Returns 0 if the conversion fails. 118 | 119 | .PARAMETER Default 120 | Default value to use if the input is null or empty. 121 | 122 | .PARAMETER Require 123 | Writes an error to the error pipeline if the input is null or empty. 124 | #> 125 | function Get-TaskVariable { 126 | [CmdletBinding(DefaultParameterSetName = 'Require')] 127 | param( 128 | [Parameter(Mandatory = $true)] 129 | [string]$Name, 130 | [Parameter(ParameterSetName = 'Default')] 131 | $Default, 132 | [Parameter(ParameterSetName = 'Require')] 133 | [switch]$Require, 134 | [switch]$AsBool, 135 | [switch]$AsInt) 136 | 137 | $originalErrorActionPreference = $ErrorActionPreference 138 | try { 139 | $ErrorActionPreference = 'Stop' 140 | $description = Get-LocString -Key PSLIB_TaskVariable0 -ArgumentList $Name 141 | $variableKey = Get-VariableKey -Name $Name 142 | if ($script:knownVariables.$variableKey.Secret) { 143 | # Get secret variable. Splatting is required to concisely invoke the correct parameter set. 144 | $null = $PSBoundParameters.Remove('Name') 145 | $vaultKey = "SECRET_$variableKey" 146 | Get-VaultValue @PSBoundParameters -Description $description -Key $vaultKey 147 | } else { 148 | # Get public variable. 149 | $item = $null 150 | $path = "Env:$variableKey" 151 | if ((Test-Path -LiteralPath $path) -and ($item = Get-Item -LiteralPath $path).Value) { 152 | # Intentionally empty. Value was successfully retrieved. 153 | } elseif (!$script:nonInteractive) { 154 | # The value wasn't found and the module is running in interactive dev mode. 155 | # Prompt for the value. 156 | Set-Item -LiteralPath $path -Value (Read-Host -Prompt $description) 157 | if (Test-Path -LiteralPath $path) { 158 | $item = Get-Item -LiteralPath $path 159 | } 160 | } 161 | 162 | # Get the converted value. Splatting is required to concisely invoke the correct parameter set. 163 | $null = $PSBoundParameters.Remove('Name') 164 | Get-Value @PSBoundParameters -Description $description -Key $variableKey -Value $item.Value 165 | } 166 | } catch { 167 | $ErrorActionPreference = $originalErrorActionPreference 168 | Write-Error $_ 169 | } 170 | } 171 | 172 | <# 173 | .SYNOPSIS 174 | Gets all job variables available to the task. Requires 2.104.1 agent or higher. 175 | 176 | .DESCRIPTION 177 | Gets a snapshot of the current state of all job variables available to the task. 178 | Requires a 2.104.1 agent or higher for full functionality. 179 | 180 | Returns an array of objects with the following properties: 181 | [string]Name 182 | [string]Value 183 | [bool]Secret 184 | 185 | Limitations on an agent prior to 2.104.1: 186 | 1) The return value does not include all public variables. Only public variables 187 | that have been added using setVariable are returned. 188 | 2) The name returned for each secret variable is the formatted environment variable 189 | name, not the actual variable name (unless it was set explicitly at runtime using 190 | setVariable). 191 | #> 192 | function Get-TaskVariableInfo { 193 | [CmdletBinding()] 194 | param() 195 | 196 | foreach ($info in $script:knownVariables.Values) { 197 | New-Object -TypeName psobject -Property @{ 198 | Name = $info.Name 199 | Value = Get-TaskVariable -Name $info.Name 200 | Secret = $info.Secret 201 | } 202 | } 203 | } 204 | 205 | <# 206 | .SYNOPSIS 207 | Sets a task variable. 208 | 209 | .DESCRIPTION 210 | Sets a task variable in the current task context as well as in the current job context. This allows the task variable to retrieved by subsequent tasks within the same job. 211 | #> 212 | function Set-TaskVariable { 213 | [CmdletBinding()] 214 | param( 215 | [Parameter(Mandatory = $true)] 216 | [string]$Name, 217 | [string]$Value, 218 | [switch]$Secret) 219 | 220 | # Once a secret always a secret. 221 | $variableKey = Get-VariableKey -Name $Name 222 | [bool]$Secret = $Secret -or $script:knownVariables.$variableKey.Secret 223 | if ($Secret) { 224 | $vaultKey = "SECRET_$variableKey" 225 | if (!$Value) { 226 | # Clear the secret. 227 | Write-Verbose "Set $Name = ''" 228 | $script:vault.Remove($vaultKey) 229 | } else { 230 | # Store the secret in the vault. 231 | Write-Verbose "Set $Name = '********'" 232 | $script:vault[$vaultKey] = New-Object System.Management.Automation.PSCredential( 233 | $vaultKey, 234 | (ConvertTo-SecureString -String $Value -AsPlainText -Force)) 235 | } 236 | 237 | # Clear the environment variable. 238 | Set-Item -LiteralPath "Env:$variableKey" -Value '' 239 | } else { 240 | # Set the environment variable. 241 | Write-Verbose "Set $Name = '$Value'" 242 | Set-Item -LiteralPath "Env:$variableKey" -Value $Value 243 | } 244 | 245 | # Store the metadata. 246 | $script:knownVariables[$variableKey] = New-Object -TypeName psobject -Property @{ 247 | Name = $name 248 | Secret = $Secret 249 | } 250 | 251 | # Persist the variable in the task context. 252 | Write-SetVariable -Name $Name -Value $Value -Secret:$Secret 253 | } 254 | 255 | ######################################## 256 | # Private functions. 257 | ######################################## 258 | function Get-VaultValue { 259 | [CmdletBinding(DefaultParameterSetName = 'Require')] 260 | param( 261 | [Parameter(Mandatory = $true)] 262 | [string]$Description, 263 | [Parameter(Mandatory = $true)] 264 | [string]$Key, 265 | [Parameter(ParameterSetName = 'Require')] 266 | [switch]$Require, 267 | [Parameter(ParameterSetName = 'Default')] 268 | [object]$Default, 269 | [switch]$AsBool, 270 | [switch]$AsInt) 271 | 272 | # Attempt to get the vault value. 273 | $value = $null 274 | if ($psCredential = $script:vault[$Key]) { 275 | $value = $psCredential.GetNetworkCredential().Password 276 | } elseif (!$script:nonInteractive) { 277 | # The value wasn't found. Prompt for the value if running in interactive dev mode. 278 | $value = Read-Host -Prompt $Description 279 | if ($value) { 280 | $script:vault[$Key] = New-Object System.Management.Automation.PSCredential( 281 | $Key, 282 | (ConvertTo-SecureString -String $value -AsPlainText -Force)) 283 | } 284 | } 285 | 286 | Get-Value -Value $value @PSBoundParameters 287 | } 288 | 289 | function Get-Value { 290 | [CmdletBinding(DefaultParameterSetName = 'Require')] 291 | param( 292 | [string]$Value, 293 | [Parameter(Mandatory = $true)] 294 | [string]$Description, 295 | [Parameter(Mandatory = $true)] 296 | [string]$Key, 297 | [Parameter(ParameterSetName = 'Require')] 298 | [switch]$Require, 299 | [Parameter(ParameterSetName = 'Default')] 300 | [object]$Default, 301 | [switch]$AsBool, 302 | [switch]$AsInt) 303 | 304 | $result = $Value 305 | if ($result) { 306 | if ($Key -like 'ENDPOINT_AUTH_*') { 307 | Write-Verbose "$($Key): '********'" 308 | } else { 309 | Write-Verbose "$($Key): '$result'" 310 | } 311 | } else { 312 | Write-Verbose "$Key (empty)" 313 | 314 | # Write error if required. 315 | if ($Require) { 316 | Write-Error "$(Get-LocString -Key PSLIB_Required0 $Description)" 317 | return 318 | } 319 | 320 | # Fallback to the default if provided. 321 | if ($PSCmdlet.ParameterSetName -eq 'Default') { 322 | $result = $Default 323 | $OFS = ' ' 324 | Write-Verbose " Defaulted to: '$result'" 325 | } else { 326 | $result = '' 327 | } 328 | } 329 | 330 | # Convert to bool if specified. 331 | if ($AsBool) { 332 | if ($result -isnot [bool]) { 333 | $result = "$result" -in '1', 'true' 334 | Write-Verbose " Converted to bool: $result" 335 | } 336 | 337 | return $result 338 | } 339 | 340 | # Convert to int if specified. 341 | if ($AsInt) { 342 | if ($result -isnot [int]) { 343 | try { 344 | $result = [int]"$result" 345 | } catch { 346 | $result = 0 347 | } 348 | 349 | Write-Verbose " Converted to int: $result" 350 | } 351 | 352 | return $result 353 | } 354 | 355 | return $result 356 | } 357 | 358 | function Initialize-Inputs { 359 | # Store endpoints, inputs, and secret variables in the vault. 360 | foreach ($variable in (Get-ChildItem -Path Env:ENDPOINT_?*, Env:INPUT_?*, Env:SECRET_?*, Env:SECUREFILE_?*)) { 361 | # Record the secret variable metadata. This is required by Get-TaskVariable to 362 | # retrieve the value. In a 2.104.1 agent or higher, this metadata will be overwritten 363 | # when $env:VSTS_SECRET_VARIABLES is processed. 364 | if ($variable.Name -like 'SECRET_?*') { 365 | $variableKey = $variable.Name.Substring('SECRET_'.Length) 366 | $script:knownVariables[$variableKey] = New-Object -TypeName psobject -Property @{ 367 | # This is technically not the variable name (has underscores instead of dots), 368 | # but it's good enough to make Get-TaskVariable work in a pre-2.104.1 agent 369 | # where $env:VSTS_SECRET_VARIABLES is not defined. 370 | Name = $variableKey 371 | Secret = $true 372 | } 373 | } 374 | 375 | # Store the value in the vault. 376 | $vaultKey = $variable.Name 377 | if ($variable.Value) { 378 | $script:vault[$vaultKey] = New-Object System.Management.Automation.PSCredential( 379 | $vaultKey, 380 | (ConvertTo-SecureString -String $variable.Value -AsPlainText -Force)) 381 | } 382 | 383 | # Clear the environment variable. 384 | Remove-Item -LiteralPath "Env:$($variable.Name)" 385 | } 386 | 387 | # Record the public variable names. Env var added in 2.104.1 agent. 388 | if ($env:VSTS_PUBLIC_VARIABLES) { 389 | foreach ($name in (ConvertFrom-Json -InputObject $env:VSTS_PUBLIC_VARIABLES)) { 390 | $variableKey = Get-VariableKey -Name $name 391 | $script:knownVariables[$variableKey] = New-Object -TypeName psobject -Property @{ 392 | Name = $name 393 | Secret = $false 394 | } 395 | } 396 | 397 | $env:VSTS_PUBLIC_VARIABLES = '' 398 | } 399 | 400 | # Record the secret variable names. Env var added in 2.104.1 agent. 401 | if ($env:VSTS_SECRET_VARIABLES) { 402 | foreach ($name in (ConvertFrom-Json -InputObject $env:VSTS_SECRET_VARIABLES)) { 403 | $variableKey = Get-VariableKey -Name $name 404 | $script:knownVariables[$variableKey] = New-Object -TypeName psobject -Property @{ 405 | Name = $name 406 | Secret = $true 407 | } 408 | } 409 | 410 | $env:VSTS_SECRET_VARIABLES = '' 411 | } 412 | } 413 | 414 | function Get-VariableKey { 415 | [CmdletBinding()] 416 | param( 417 | [Parameter(Mandatory = $true)] 418 | [string]$Name) 419 | 420 | if ($Name -ne 'agent.jobstatus') { 421 | $Name = $Name.Replace('.', '_') 422 | } 423 | 424 | $Name.ToUpperInvariant() 425 | } 426 | -------------------------------------------------------------------------------- /hugo-task/ps_modules/VstsTaskSdk/LoggingCommandFunctions.ps1: -------------------------------------------------------------------------------- 1 | $script:loggingCommandPrefix = '##vso[' 2 | $script:loggingCommandEscapeMappings = @( # TODO: WHAT ABOUT "="? WHAT ABOUT "%"? 3 | New-Object psobject -Property @{ Token = ';' ; Replacement = '%3B' } 4 | New-Object psobject -Property @{ Token = "`r" ; Replacement = '%0D' } 5 | New-Object psobject -Property @{ Token = "`n" ; Replacement = '%0A' } 6 | ) 7 | # TODO: BUG: Escape ] 8 | # TODO: BUG: Escape % ??? 9 | # TODO: Add test to verify don't need to escape "=". 10 | 11 | <# 12 | .SYNOPSIS 13 | See https://github.com/Microsoft/vsts-tasks/blob/master/docs/authoring/commands.md 14 | 15 | .PARAMETER AsOutput 16 | Indicates whether to write the logging command directly to the host or to the output pipeline. 17 | #> 18 | function Write-AddAttachment { 19 | [CmdletBinding()] 20 | param( 21 | [Parameter(Mandatory = $true)] 22 | [string]$Type, 23 | [Parameter(Mandatory = $true)] 24 | [string]$Name, 25 | [Parameter(Mandatory = $true)] 26 | [string]$Path, 27 | [switch]$AsOutput) 28 | 29 | Write-LoggingCommand -Area 'task' -Event 'addattachment' -Data $Path -Properties @{ 30 | 'type' = $Type 31 | 'name' = $Name 32 | } -AsOutput:$AsOutput 33 | } 34 | 35 | <# 36 | .SYNOPSIS 37 | See https://github.com/Microsoft/vsts-tasks/blob/master/docs/authoring/commands.md 38 | 39 | .PARAMETER AsOutput 40 | Indicates whether to write the logging command directly to the host or to the output pipeline. 41 | #> 42 | function Write-AddBuildTag { 43 | [CmdletBinding()] 44 | param( 45 | [Parameter(Mandatory = $true)] 46 | [string]$Value, 47 | [switch]$AsOutput) 48 | 49 | Write-LoggingCommand -Area 'build' -Event 'addbuildtag' -Data $Value -AsOutput:$AsOutput 50 | } 51 | 52 | <# 53 | .SYNOPSIS 54 | See https://github.com/Microsoft/vsts-tasks/blob/master/docs/authoring/commands.md 55 | 56 | .PARAMETER AsOutput 57 | Indicates whether to write the logging command directly to the host or to the output pipeline. 58 | #> 59 | function Write-AssociateArtifact { 60 | [CmdletBinding()] 61 | param( 62 | [Parameter(Mandatory = $true)] 63 | [string]$Name, 64 | [Parameter(Mandatory = $true)] 65 | [string]$Path, 66 | [Parameter(Mandatory = $true)] 67 | [string]$Type, 68 | [hashtable]$Properties, 69 | [switch]$AsOutput) 70 | 71 | $p = @{ } 72 | if ($Properties) { 73 | foreach ($key in $Properties.Keys) { 74 | $p[$key] = $Properties[$key] 75 | } 76 | } 77 | 78 | $p['artifactname'] = $Name 79 | $p['artifacttype'] = $Type 80 | Write-LoggingCommand -Area 'artifact' -Event 'associate' -Data $Path -Properties $p -AsOutput:$AsOutput 81 | } 82 | 83 | <# 84 | .SYNOPSIS 85 | See https://github.com/Microsoft/vsts-tasks/blob/master/docs/authoring/commands.md 86 | 87 | .PARAMETER AsOutput 88 | Indicates whether to write the logging command directly to the host or to the output pipeline. 89 | #> 90 | function Write-LogDetail { 91 | [CmdletBinding()] 92 | param( 93 | [Parameter(Mandatory = $true)] 94 | [guid]$Id, 95 | $ParentId, 96 | [string]$Type, 97 | [string]$Name, 98 | $Order, 99 | $StartTime, 100 | $FinishTime, 101 | $Progress, 102 | [ValidateSet('Unknown', 'Initialized', 'InProgress', 'Completed')] 103 | [Parameter()] 104 | $State, 105 | [ValidateSet('Succeeded', 'SucceededWithIssues', 'Failed', 'Cancelled', 'Skipped')] 106 | [Parameter()] 107 | $Result, 108 | [string]$Message, 109 | [switch]$AsOutput) 110 | 111 | Write-LoggingCommand -Area 'task' -Event 'logdetail' -Data $Message -Properties @{ 112 | 'id' = $Id 113 | 'parentid' = $ParentId 114 | 'type' = $Type 115 | 'name' = $Name 116 | 'order' = $Order 117 | 'starttime' = $StartTime 118 | 'finishtime' = $FinishTime 119 | 'progress' = $Progress 120 | 'state' = $State 121 | 'result' = $Result 122 | } -AsOutput:$AsOutput 123 | } 124 | 125 | <# 126 | .SYNOPSIS 127 | See https://github.com/Microsoft/vsts-tasks/blob/master/docs/authoring/commands.md 128 | 129 | .PARAMETER AsOutput 130 | Indicates whether to write the logging command directly to the host or to the output pipeline. 131 | #> 132 | function Write-SetProgress { 133 | [CmdletBinding()] 134 | param( 135 | [ValidateRange(0, 100)] 136 | [Parameter(Mandatory = $true)] 137 | [int]$Percent, 138 | [string]$CurrentOperation, 139 | [switch]$AsOutput) 140 | 141 | Write-LoggingCommand -Area 'task' -Event 'setprogress' -Data $CurrentOperation -Properties @{ 142 | 'value' = $Percent 143 | } -AsOutput:$AsOutput 144 | } 145 | 146 | <# 147 | .SYNOPSIS 148 | See https://github.com/Microsoft/vsts-tasks/blob/master/docs/authoring/commands.md 149 | 150 | .PARAMETER AsOutput 151 | Indicates whether to write the logging command directly to the host or to the output pipeline. 152 | #> 153 | function Write-SetResult { 154 | [CmdletBinding(DefaultParameterSetName = 'AsOutput')] 155 | param( 156 | [ValidateSet("Succeeded", "SucceededWithIssues", "Failed", "Cancelled", "Skipped")] 157 | [Parameter(Mandatory = $true)] 158 | [string]$Result, 159 | [string]$Message, 160 | [Parameter(ParameterSetName = 'AsOutput')] 161 | [switch]$AsOutput, 162 | [Parameter(ParameterSetName = 'DoNotThrow')] 163 | [switch]$DoNotThrow) 164 | 165 | Write-LoggingCommand -Area 'task' -Event 'complete' -Data $Message -Properties @{ 166 | 'result' = $Result 167 | } -AsOutput:$AsOutput 168 | if ($Result -eq 'Failed' -and !$AsOutput -and !$DoNotThrow) { 169 | # Special internal exception type to control the flow. Not currently intended 170 | # for public usage and subject to change. 171 | throw (New-Object VstsTaskSdk.TerminationException($Message)) 172 | } 173 | } 174 | 175 | <# 176 | .SYNOPSIS 177 | See https://github.com/Microsoft/vsts-tasks/blob/master/docs/authoring/commands.md 178 | 179 | .PARAMETER AsOutput 180 | Indicates whether to write the logging command directly to the host or to the output pipeline. 181 | #> 182 | function Write-SetSecret { 183 | [CmdletBinding()] 184 | param( 185 | [Parameter(Mandatory = $true)] 186 | [string]$Value, 187 | [switch]$AsOutput) 188 | 189 | Write-LoggingCommand -Area 'task' -Event 'setsecret' -Data $Value -AsOutput:$AsOutput 190 | } 191 | 192 | <# 193 | .SYNOPSIS 194 | See https://github.com/Microsoft/vsts-tasks/blob/master/docs/authoring/commands.md 195 | 196 | .PARAMETER AsOutput 197 | Indicates whether to write the logging command directly to the host or to the output pipeline. 198 | #> 199 | function Write-SetVariable { 200 | [CmdletBinding()] 201 | param( 202 | [Parameter(Mandatory = $true)] 203 | [string]$Name, 204 | [string]$Value, 205 | [switch]$Secret, 206 | [switch]$AsOutput) 207 | 208 | Write-LoggingCommand -Area 'task' -Event 'setvariable' -Data $Value -Properties @{ 209 | 'variable' = $Name 210 | 'issecret' = $Secret 211 | } -AsOutput:$AsOutput 212 | } 213 | 214 | <# 215 | .SYNOPSIS 216 | See https://github.com/Microsoft/vsts-tasks/blob/master/docs/authoring/commands.md 217 | 218 | .PARAMETER AsOutput 219 | Indicates whether to write the logging command directly to the host or to the output pipeline. 220 | #> 221 | function Write-TaskDebug { 222 | [CmdletBinding()] 223 | param( 224 | [string]$Message, 225 | [switch]$AsOutput) 226 | 227 | Write-TaskDebug_Internal @PSBoundParameters 228 | } 229 | 230 | <# 231 | .SYNOPSIS 232 | See https://github.com/Microsoft/vsts-tasks/blob/master/docs/authoring/commands.md 233 | 234 | .PARAMETER AsOutput 235 | Indicates whether to write the logging command directly to the host or to the output pipeline. 236 | #> 237 | function Write-TaskError { 238 | [CmdletBinding()] 239 | param( 240 | [string]$Message, 241 | [string]$ErrCode, 242 | [string]$SourcePath, 243 | [string]$LineNumber, 244 | [string]$ColumnNumber, 245 | [switch]$AsOutput) 246 | 247 | Write-LogIssue -Type error @PSBoundParameters 248 | } 249 | 250 | <# 251 | .SYNOPSIS 252 | See https://github.com/Microsoft/vsts-tasks/blob/master/docs/authoring/commands.md 253 | 254 | .PARAMETER AsOutput 255 | Indicates whether to write the logging command directly to the host or to the output pipeline. 256 | #> 257 | function Write-TaskVerbose { 258 | [CmdletBinding()] 259 | param( 260 | [string]$Message, 261 | [switch]$AsOutput) 262 | 263 | Write-TaskDebug_Internal @PSBoundParameters -AsVerbose 264 | } 265 | 266 | <# 267 | .SYNOPSIS 268 | See https://github.com/Microsoft/vsts-tasks/blob/master/docs/authoring/commands.md 269 | 270 | .PARAMETER AsOutput 271 | Indicates whether to write the logging command directly to the host or to the output pipeline. 272 | #> 273 | function Write-TaskWarning { 274 | [CmdletBinding()] 275 | param( 276 | [string]$Message, 277 | [string]$ErrCode, 278 | [string]$SourcePath, 279 | [string]$LineNumber, 280 | [string]$ColumnNumber, 281 | [switch]$AsOutput) 282 | 283 | Write-LogIssue -Type warning @PSBoundParameters 284 | } 285 | 286 | <# 287 | .SYNOPSIS 288 | See https://github.com/Microsoft/vsts-tasks/blob/master/docs/authoring/commands.md 289 | 290 | .PARAMETER AsOutput 291 | Indicates whether to write the logging command directly to the host or to the output pipeline. 292 | #> 293 | function Write-UpdateBuildNumber { 294 | [CmdletBinding()] 295 | param( 296 | [Parameter(Mandatory = $true)] 297 | [string]$Value, 298 | [switch]$AsOutput) 299 | 300 | Write-LoggingCommand -Area 'build' -Event 'updatebuildnumber' -Data $Value -AsOutput:$AsOutput 301 | } 302 | 303 | <# 304 | .SYNOPSIS 305 | See https://github.com/Microsoft/vsts-tasks/blob/master/docs/authoring/commands.md 306 | 307 | .PARAMETER AsOutput 308 | Indicates whether to write the logging command directly to the host or to the output pipeline. 309 | #> 310 | function Write-UploadArtifact { 311 | [CmdletBinding()] 312 | param( 313 | [Parameter(Mandatory = $true)] 314 | [string]$ContainerFolder, 315 | [Parameter(Mandatory = $true)] 316 | [string]$Name, 317 | [Parameter(Mandatory = $true)] 318 | [string]$Path, 319 | [switch]$AsOutput) 320 | 321 | Write-LoggingCommand -Area 'artifact' -Event 'upload' -Data $Path -Properties @{ 322 | 'containerfolder' = $ContainerFolder 323 | 'artifactname' = $Name 324 | } -AsOutput:$AsOutput 325 | } 326 | 327 | <# 328 | .SYNOPSIS 329 | See https://github.com/Microsoft/vsts-tasks/blob/master/docs/authoring/commands.md 330 | 331 | .PARAMETER AsOutput 332 | Indicates whether to write the logging command directly to the host or to the output pipeline. 333 | #> 334 | function Write-UploadBuildLog { 335 | [CmdletBinding()] 336 | param( 337 | [Parameter(Mandatory = $true)] 338 | [string]$Path, 339 | [switch]$AsOutput) 340 | 341 | Write-LoggingCommand -Area 'build' -Event 'uploadlog' -Data $Path -AsOutput:$AsOutput 342 | } 343 | 344 | ######################################## 345 | # Private functions. 346 | ######################################## 347 | function Format-LoggingCommandData { 348 | [CmdletBinding()] 349 | param([string]$Value, [switch]$Reverse) 350 | 351 | if (!$Value) { 352 | return '' 353 | } 354 | 355 | if (!$Reverse) { 356 | foreach ($mapping in $script:loggingCommandEscapeMappings) { 357 | $Value = $Value.Replace($mapping.Token, $mapping.Replacement) 358 | } 359 | } else { 360 | for ($i = $script:loggingCommandEscapeMappings.Length - 1 ; $i -ge 0 ; $i--) { 361 | $mapping = $script:loggingCommandEscapeMappings[$i] 362 | $Value = $Value.Replace($mapping.Replacement, $mapping.Token) 363 | } 364 | } 365 | 366 | return $Value 367 | } 368 | 369 | function Format-LoggingCommand { 370 | [CmdletBinding()] 371 | param( 372 | [Parameter(Mandatory = $true)] 373 | [string]$Area, 374 | [Parameter(Mandatory = $true)] 375 | [string]$Event, 376 | [string]$Data, 377 | [hashtable]$Properties) 378 | 379 | # Append the preamble. 380 | [System.Text.StringBuilder]$sb = New-Object -TypeName System.Text.StringBuilder 381 | $null = $sb.Append($script:loggingCommandPrefix).Append($Area).Append('.').Append($Event) 382 | 383 | # Append the properties. 384 | if ($Properties) { 385 | $first = $true 386 | foreach ($key in $Properties.Keys) { 387 | [string]$value = Format-LoggingCommandData $Properties[$key] 388 | if ($value) { 389 | if ($first) { 390 | $null = $sb.Append(' ') 391 | $first = $false 392 | } else { 393 | $null = $sb.Append(';') 394 | } 395 | 396 | $null = $sb.Append("$key=$value") 397 | } 398 | } 399 | } 400 | 401 | # Append the tail and output the value. 402 | $Data = Format-LoggingCommandData $Data 403 | $sb.Append(']').Append($Data).ToString() 404 | } 405 | 406 | function Write-LoggingCommand { 407 | [CmdletBinding(DefaultParameterSetName = 'Parameters')] 408 | param( 409 | [Parameter(Mandatory = $true, ParameterSetName = 'Parameters')] 410 | [string]$Area, 411 | [Parameter(Mandatory = $true, ParameterSetName = 'Parameters')] 412 | [string]$Event, 413 | [Parameter(ParameterSetName = 'Parameters')] 414 | [string]$Data, 415 | [Parameter(ParameterSetName = 'Parameters')] 416 | [hashtable]$Properties, 417 | [Parameter(Mandatory = $true, ParameterSetName = 'Object')] 418 | $Command, 419 | [switch]$AsOutput) 420 | 421 | if ($PSCmdlet.ParameterSetName -eq 'Object') { 422 | Write-LoggingCommand -Area $Command.Area -Event $Command.Event -Data $Command.Data -Properties $Command.Properties -AsOutput:$AsOutput 423 | return 424 | } 425 | 426 | $command = Format-LoggingCommand -Area $Area -Event $Event -Data $Data -Properties $Properties 427 | if ($AsOutput) { 428 | $command 429 | } else { 430 | Write-Host $command 431 | } 432 | } 433 | 434 | function Write-LogIssue { 435 | [CmdletBinding()] 436 | param( 437 | [ValidateSet('warning', 'error')] 438 | [Parameter(Mandatory = $true)] 439 | [string]$Type, 440 | [string]$Message, 441 | [string]$ErrCode, 442 | [string]$SourcePath, 443 | [string]$LineNumber, 444 | [string]$ColumnNumber, 445 | [switch]$AsOutput) 446 | 447 | $command = Format-LoggingCommand -Area 'task' -Event 'logissue' -Data $Message -Properties @{ 448 | 'type' = $Type 449 | 'code' = $ErrCode 450 | 'sourcepath' = $SourcePath 451 | 'linenumber' = $LineNumber 452 | 'columnnumber' = $ColumnNumber 453 | } 454 | if ($AsOutput) { 455 | return $command 456 | } 457 | 458 | if ($Type -eq 'error') { 459 | $foregroundColor = $host.PrivateData.ErrorForegroundColor 460 | $backgroundColor = $host.PrivateData.ErrorBackgroundColor 461 | if ($foregroundColor -isnot [System.ConsoleColor] -or $backgroundColor -isnot [System.ConsoleColor]) { 462 | $foregroundColor = [System.ConsoleColor]::Red 463 | $backgroundColor = [System.ConsoleColor]::Black 464 | } 465 | } else { 466 | $foregroundColor = $host.PrivateData.WarningForegroundColor 467 | $backgroundColor = $host.PrivateData.WarningBackgroundColor 468 | if ($foregroundColor -isnot [System.ConsoleColor] -or $backgroundColor -isnot [System.ConsoleColor]) { 469 | $foregroundColor = [System.ConsoleColor]::Yellow 470 | $backgroundColor = [System.ConsoleColor]::Black 471 | } 472 | } 473 | 474 | Write-Host $command -ForegroundColor $foregroundColor -BackgroundColor $backgroundColor 475 | } 476 | 477 | function Write-TaskDebug_Internal { 478 | [CmdletBinding()] 479 | param( 480 | [string]$Message, 481 | [switch]$AsVerbose, 482 | [switch]$AsOutput) 483 | 484 | $command = Format-LoggingCommand -Area 'task' -Event 'debug' -Data $Message 485 | if ($AsOutput) { 486 | return $command 487 | } 488 | 489 | if ($AsVerbose) { 490 | $foregroundColor = $host.PrivateData.VerboseForegroundColor 491 | $backgroundColor = $host.PrivateData.VerboseBackgroundColor 492 | if ($foregroundColor -isnot [System.ConsoleColor] -or $backgroundColor -isnot [System.ConsoleColor]) { 493 | $foregroundColor = [System.ConsoleColor]::Cyan 494 | $backgroundColor = [System.ConsoleColor]::Black 495 | } 496 | } else { 497 | $foregroundColor = $host.PrivateData.DebugForegroundColor 498 | $backgroundColor = $host.PrivateData.DebugBackgroundColor 499 | if ($foregroundColor -isnot [System.ConsoleColor] -or $backgroundColor -isnot [System.ConsoleColor]) { 500 | $foregroundColor = [System.ConsoleColor]::DarkGray 501 | $backgroundColor = [System.ConsoleColor]::Black 502 | } 503 | } 504 | 505 | Write-Host -Object $command -ForegroundColor $foregroundColor -BackgroundColor $backgroundColor 506 | } 507 | -------------------------------------------------------------------------------- /hugo-task/ps_modules/VstsTaskSdk/ServerOMFunctions.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Gets assembly reference information. 4 | 5 | .DESCRIPTION 6 | Not supported for use during task exection. This function is only intended to help developers resolve the minimal set of DLLs that need to be bundled when consuming the VSTS REST SDK or TFS Extended Client SDK. The interface and output may change between patch releases of the VSTS Task SDK. 7 | 8 | Only a subset of the referenced assemblies may actually be required, depending on the functionality used by your task. It is best to bundle only the DLLs required for your scenario. 9 | 10 | Walks an assembly's references to determine all of it's dependencies. Also walks the references of the dependencies, and so on until all nested dependencies have been traversed. Dependencies are searched for in the directory of the specified assembly. NET Framework assemblies are omitted. 11 | 12 | See https://github.com/Microsoft/vsts-task-lib/tree/master/powershell/Docs/UsingOM.md for reliable usage when working with the TFS extended client SDK from a task. 13 | 14 | .PARAMETER LiteralPath 15 | Assembly to walk. 16 | 17 | .EXAMPLE 18 | Get-VstsAssemblyReference -LiteralPath C:\nuget\microsoft.teamfoundationserver.client.14.102.0\lib\net45\Microsoft.TeamFoundation.Build2.WebApi.dll 19 | #> 20 | function Get-AssemblyReference { 21 | [CmdletBinding()] 22 | param( 23 | [Parameter(Mandatory = $true)] 24 | [string]$LiteralPath) 25 | 26 | $ErrorActionPreference = 'Stop' 27 | Write-Warning "Not supported for use during task exection. This function is only intended to help developers resolve the minimal set of DLLs that need to be bundled when consuming the VSTS REST SDK or TFS Extended Client SDK. The interface and output may change between patch releases of the VSTS Task SDK." 28 | Write-Output '' 29 | Write-Warning "Only a subset of the referenced assemblies may actually be required, depending on the functionality used by your task. It is best to bundle only the DLLs required for your scenario." 30 | $directory = [System.IO.Path]::GetDirectoryName($LiteralPath) 31 | $hashtable = @{ } 32 | $queue = @( [System.Reflection.Assembly]::ReflectionOnlyLoadFrom($LiteralPath).GetName() ) 33 | while ($queue.Count) { 34 | # Add a blank line between assemblies. 35 | Write-Output '' 36 | 37 | # Pop. 38 | $assemblyName = $queue[0] 39 | $queue = @( $queue | Select-Object -Skip 1 ) 40 | 41 | # Attempt to find the assembly in the same directory. 42 | $assembly = $null 43 | $path = "$directory\$($assemblyName.Name).dll" 44 | if ((Test-Path -LiteralPath $path -PathType Leaf)) { 45 | $assembly = [System.Reflection.Assembly]::ReflectionOnlyLoadFrom($path) 46 | } else { 47 | $path = "$directory\$($assemblyName.Name).exe" 48 | if ((Test-Path -LiteralPath $path -PathType Leaf)) { 49 | $assembly = [System.Reflection.Assembly]::ReflectionOnlyLoadFrom($path) 50 | } 51 | } 52 | 53 | # Make sure the assembly full name matches, not just the file name. 54 | if ($assembly -and $assembly.GetName().FullName -ne $assemblyName.FullName) { 55 | $assembly = $null 56 | } 57 | 58 | # Print the assembly. 59 | if ($assembly) { 60 | Write-Output $assemblyName.FullName 61 | } else { 62 | if ($assemblyName.FullName -eq 'Newtonsoft.Json, Version=6.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed') { 63 | Write-Warning "*** NOT FOUND $($assemblyName.FullName) *** This is an expected condition when using the HTTP clients from the 15.x VSTS REST SDK. Use Get-VstsVssHttpClient to load the HTTP clients (which applies a binding redirect assembly resolver for Newtonsoft.Json). Otherwise you will need to manage the binding redirect yourself." 64 | } else { 65 | Write-Warning "*** NOT FOUND $($assemblyName.FullName) ***" 66 | } 67 | 68 | continue 69 | } 70 | 71 | # Walk the references. 72 | $refAssemblyNames = @( $assembly.GetReferencedAssemblies() ) 73 | for ($i = 0 ; $i -lt $refAssemblyNames.Count ; $i++) { 74 | $refAssemblyName = $refAssemblyNames[$i] 75 | 76 | # Skip framework assemblies. 77 | $fxPaths = @( 78 | "$env:windir\Microsoft.Net\Framework64\v4.0.30319\$($refAssemblyName.Name).dll" 79 | "$env:windir\Microsoft.Net\Framework64\v4.0.30319\WPF\$($refAssemblyName.Name).dll" 80 | ) 81 | $fxPath = $fxPaths | 82 | Where-Object { Test-Path -LiteralPath $_ -PathType Leaf } | 83 | Where-Object { [System.Reflection.Assembly]::ReflectionOnlyLoadFrom($_).GetName().FullName -eq $refAssemblyName.FullName } 84 | if ($fxPath) { 85 | continue 86 | } 87 | 88 | # Print the reference. 89 | Write-Output " $($refAssemblyName.FullName)" 90 | 91 | # Add new references to the queue. 92 | if (!$hashtable[$refAssemblyName.FullName]) { 93 | $queue += $refAssemblyName 94 | $hashtable[$refAssemblyName.FullName] = $true 95 | } 96 | } 97 | } 98 | } 99 | 100 | <# 101 | .SYNOPSIS 102 | Gets a credentials object that can be used with the TFS extended client SDK. 103 | 104 | .DESCRIPTION 105 | The agent job token is used to construct the credentials object. The identity associated with the token depends on the scope selected in the build/release definition (either the project collection build/release service identity, or the project build/release service identity). 106 | 107 | Refer to Get-VstsTfsService for a more simple to get a TFS service object. 108 | 109 | *** DO NOT USE Agent.ServerOMDirectory *** See https://github.com/Microsoft/vsts-task-lib/tree/master/powershell/Docs/UsingOM.md for reliable usage when working with the TFS extended client SDK from a task. 110 | 111 | .PARAMETER OMDirectory 112 | Directory where the extended client object model DLLs are located. If the DLLs for the credential types are not already loaded, an attempt will be made to automatically load the required DLLs from the object model directory. 113 | 114 | If not specified, defaults to the directory of the entry script for the task. 115 | 116 | *** DO NOT USE Agent.ServerOMDirectory *** See https://github.com/Microsoft/vsts-task-lib/tree/master/powershell/Docs/UsingOM.md for reliable usage when working with the TFS extended client SDK from a task. 117 | 118 | .EXAMPLE 119 | # 120 | # Refer to Get-VstsTfsService for a more simple way to get a TFS service object. 121 | # 122 | $credentials = Get-VstsTfsClientCredentials 123 | Add-Type -LiteralPath "$PSScriptRoot\Microsoft.TeamFoundation.VersionControl.Client.dll" 124 | $tfsTeamProjectCollection = New-Object Microsoft.TeamFoundation.Client.TfsTeamProjectCollection( 125 | (Get-VstsTaskVariable -Name 'System.TeamFoundationCollectionUri' -Require), 126 | $credentials) 127 | $versionControlServer = $tfsTeamProjectCollection.GetService([Microsoft.TeamFoundation.VersionControl.Client.VersionControlServer]) 128 | $versionControlServer.GetItems('$/*').Items | Format-List 129 | #> 130 | function Get-TfsClientCredentials { 131 | [CmdletBinding()] 132 | param([string]$OMDirectory) 133 | 134 | Trace-EnteringInvocation -InvocationInfo $MyInvocation 135 | $originalErrorActionPreference = $ErrorActionPreference 136 | try { 137 | $ErrorActionPreference = 'Stop' 138 | 139 | # Get the endpoint. 140 | $endpoint = Get-Endpoint -Name SystemVssConnection -Require 141 | 142 | # Validate the type can be found. 143 | $null = Get-OMType -TypeName 'Microsoft.TeamFoundation.Client.TfsClientCredentials' -OMKind 'ExtendedClient' -OMDirectory $OMDirectory -Require 144 | 145 | # Construct the credentials. 146 | $credentials = New-Object Microsoft.TeamFoundation.Client.TfsClientCredentials($false) # Do not use default credentials. 147 | $credentials.AllowInteractive = $false 148 | $credentials.Federated = New-Object Microsoft.TeamFoundation.Client.OAuthTokenCredential([string]$endpoint.auth.parameters.AccessToken) 149 | return $credentials 150 | } catch { 151 | $ErrorActionPreference = $originalErrorActionPreference 152 | Write-Error $_ 153 | } finally { 154 | Trace-LeavingInvocation -InvocationInfo $MyInvocation 155 | } 156 | } 157 | 158 | <# 159 | .SYNOPSIS 160 | Gets a TFS extended client service. 161 | 162 | .DESCRIPTION 163 | Gets an instance of an ITfsTeamProjectCollectionObject. 164 | 165 | *** DO NOT USE Agent.ServerOMDirectory *** See https://github.com/Microsoft/vsts-task-lib/tree/master/powershell/Docs/UsingOM.md for reliable usage when working with the TFS extended client SDK from a task. 166 | 167 | .PARAMETER TypeName 168 | Namespace-qualified type name of the service to get. 169 | 170 | .PARAMETER OMDirectory 171 | Directory where the extended client object model DLLs are located. If the DLLs for the types are not already loaded, an attempt will be made to automatically load the required DLLs from the object model directory. 172 | 173 | If not specified, defaults to the directory of the entry script for the task. 174 | 175 | *** DO NOT USE Agent.ServerOMDirectory *** See https://github.com/Microsoft/vsts-task-lib/tree/master/powershell/Docs/UsingOM.md for reliable usage when working with the TFS extended client SDK from a task. 176 | 177 | .PARAMETER Uri 178 | URI to use when initializing the service. If not specified, defaults to System.TeamFoundationCollectionUri. 179 | 180 | .PARAMETER TfsClientCredentials 181 | Credentials to use when intializing the service. If not specified, the default uses the agent job token to construct the credentials object. The identity associated with the token depends on the scope selected in the build/release definition (either the project collection build/release service identity, or the project build/release service identity). 182 | 183 | .EXAMPLE 184 | $versionControlServer = Get-VstsTfsService -TypeName Microsoft.TeamFoundation.VersionControl.Client.VersionControlServer 185 | $versionControlServer.GetItems('$/*').Items | Format-List 186 | #> 187 | function Get-TfsService { 188 | [CmdletBinding()] 189 | param( 190 | [Parameter(Mandatory = $true)] 191 | [string]$TypeName, 192 | 193 | [string]$OMDirectory, 194 | 195 | [string]$Uri, 196 | 197 | $TfsClientCredentials) 198 | 199 | Trace-EnteringInvocation -InvocationInfo $MyInvocation 200 | $originalErrorActionPreference = $ErrorActionPreference 201 | try { 202 | $ErrorActionPreference = 'Stop' 203 | 204 | # Default the URI to the collection URI. 205 | if (!$Uri) { 206 | $Uri = Get-TaskVariable -Name System.TeamFoundationCollectionUri -Require 207 | } 208 | 209 | # Default the credentials. 210 | if (!$TfsClientCredentials) { 211 | $TfsClientCredentials = Get-TfsClientCredentials -OMDirectory $OMDirectory 212 | } 213 | 214 | # Validate the project collection type can be loaded. 215 | $null = Get-OMType -TypeName 'Microsoft.TeamFoundation.Client.TfsTeamProjectCollection' -OMKind 'ExtendedClient' -OMDirectory $OMDirectory -Require 216 | 217 | # Load the project collection object. 218 | $tfsTeamProjectCollection = New-Object Microsoft.TeamFoundation.Client.TfsTeamProjectCollection($Uri, $TfsClientCredentials) 219 | 220 | # Validate the requested type can be loaded. 221 | $type = Get-OMType -TypeName $TypeName -OMKind 'ExtendedClient' -OMDirectory $OMDirectory -Require 222 | 223 | # Return the service object. 224 | return $tfsTeamProjectCollection.GetService($type) 225 | } catch { 226 | $ErrorActionPreference = $originalErrorActionPreference 227 | Write-Error $_ 228 | } finally { 229 | Trace-LeavingInvocation -InvocationInfo $MyInvocation 230 | } 231 | } 232 | 233 | <# 234 | .SYNOPSIS 235 | Gets a credentials object that can be used with the VSTS REST SDK. 236 | 237 | .DESCRIPTION 238 | The agent job token is used to construct the credentials object. The identity associated with the token depends on the scope selected in the build/release definition (either the project collection build/release service identity, or the project service build/release identity). 239 | 240 | Refer to Get-VstsVssHttpClient for a more simple to get a VSS HTTP client. 241 | 242 | *** DO NOT USE Agent.ServerOMDirectory *** See https://github.com/Microsoft/vsts-task-lib/tree/master/powershell/Docs/UsingOM.md for reliable usage when working with the VSTS REST SDK from a task. 243 | 244 | .PARAMETER OMDirectory 245 | Directory where the REST client object model DLLs are located. If the DLLs for the credential types are not already loaded, an attempt will be made to automatically load the required DLLs from the object model directory. 246 | 247 | If not specified, defaults to the directory of the entry script for the task. 248 | 249 | *** DO NOT USE Agent.ServerOMDirectory *** See https://github.com/Microsoft/vsts-task-lib/tree/master/powershell/Docs/UsingOM.md for reliable usage when working with the VSTS REST SDK from a task. 250 | 251 | .EXAMPLE 252 | # 253 | # Refer to Get-VstsTfsService for a more simple way to get a TFS service object. 254 | # 255 | # This example works using the 14.x .NET SDK. A Newtonsoft.Json 6.0 to 8.0 binding 256 | # redirect may be required when working with the 15.x SDK. Or use Get-VstsVssHttpClient 257 | # to avoid managing the binding redirect. 258 | # 259 | $vssCredentials = Get-VstsVssCredentials 260 | $collectionUrl = New-Object System.Uri((Get-VstsTaskVariable -Name 'System.TeamFoundationCollectionUri' -Require)) 261 | Add-Type -LiteralPath "$PSScriptRoot\Microsoft.TeamFoundation.Core.WebApi.dll" 262 | $projectHttpClient = New-Object Microsoft.TeamFoundation.Core.WebApi.ProjectHttpClient($collectionUrl, $vssCredentials) 263 | $projectHttpClient.GetProjects().Result 264 | #> 265 | function Get-VssCredentials { 266 | [CmdletBinding()] 267 | param([string]$OMDirectory) 268 | 269 | Trace-EnteringInvocation -InvocationInfo $MyInvocation 270 | $originalErrorActionPreference = $ErrorActionPreference 271 | try { 272 | $ErrorActionPreference = 'Stop' 273 | 274 | # Get the endpoint. 275 | $endpoint = Get-Endpoint -Name SystemVssConnection -Require 276 | 277 | # Check if the VssOAuthAccessTokenCredential type is available. 278 | if ((Get-OMType -TypeName 'Microsoft.VisualStudio.Services.OAuth.VssOAuthAccessTokenCredential' -OMKind 'WebApi' -OMDirectory $OMDirectory)) { 279 | # Create the federated credential. 280 | $federatedCredential = New-Object Microsoft.VisualStudio.Services.OAuth.VssOAuthAccessTokenCredential($endpoint.auth.parameters.AccessToken) 281 | } else { 282 | # Validate the fallback type can be loaded. 283 | $null = Get-OMType -TypeName 'Microsoft.VisualStudio.Services.Client.VssOAuthCredential' -OMKind 'WebApi' -OMDirectory $OMDirectory -Require 284 | 285 | # Create the federated credential. 286 | $federatedCredential = New-Object Microsoft.VisualStudio.Services.Client.VssOAuthCredential($endpoint.auth.parameters.AccessToken) 287 | } 288 | 289 | # Return the credentials. 290 | return New-Object Microsoft.VisualStudio.Services.Common.VssCredentials( 291 | (New-Object Microsoft.VisualStudio.Services.Common.WindowsCredential($false)), # Do not use default credentials. 292 | $federatedCredential, 293 | [Microsoft.VisualStudio.Services.Common.CredentialPromptType]::DoNotPrompt) 294 | } catch { 295 | $ErrorActionPreference = $originalErrorActionPreference 296 | Write-Error $_ 297 | } finally { 298 | Trace-LeavingInvocation -InvocationInfo $MyInvocation 299 | } 300 | } 301 | 302 | <# 303 | .SYNOPSIS 304 | Gets a VSS HTTP client. 305 | 306 | .DESCRIPTION 307 | Gets an instance of an VSS HTTP client. 308 | 309 | *** DO NOT USE Agent.ServerOMDirectory *** See https://github.com/Microsoft/vsts-task-lib/tree/master/powershell/Docs/UsingOM.md for reliable usage when working with the VSTS REST SDK from a task. 310 | 311 | .PARAMETER TypeName 312 | Namespace-qualified type name of the HTTP client to get. 313 | 314 | .PARAMETER OMDirectory 315 | Directory where the REST client object model DLLs are located. If the DLLs for the credential types are not already loaded, an attempt will be made to automatically load the required DLLs from the object model directory. 316 | 317 | If not specified, defaults to the directory of the entry script for the task. 318 | 319 | *** DO NOT USE Agent.ServerOMDirectory *** See https://github.com/Microsoft/vsts-task-lib/tree/master/powershell/Docs/UsingOM.md for reliable usage when working with the VSTS REST SDK from a task. 320 | 321 | # .PARAMETER Uri 322 | # URI to use when initializing the HTTP client. If not specified, defaults to System.TeamFoundationCollectionUri. 323 | 324 | # .PARAMETER VssCredentials 325 | # Credentials to use when intializing the HTTP client. If not specified, the default uses the agent job token to construct the credentials object. The identity associated with the token depends on the scope selected in the build/release definition (either the project collection build/release service identity, or the project build/release service identity). 326 | 327 | # .PARAMETER WebProxy 328 | # WebProxy to use when intializing the HTTP client. If not specified, the default uses the proxy configuration agent current has. 329 | 330 | .EXAMPLE 331 | $projectHttpClient = Get-VstsVssHttpClient -TypeName Microsoft.TeamFoundation.Core.WebApi.ProjectHttpClient 332 | $projectHttpClient.GetProjects().Result 333 | #> 334 | function Get-VssHttpClient { 335 | [CmdletBinding()] 336 | param( 337 | [Parameter(Mandatory = $true)] 338 | [string]$TypeName, 339 | 340 | [string]$OMDirectory, 341 | 342 | [string]$Uri, 343 | 344 | $VssCredentials, 345 | 346 | $WebProxy = (Get-WebProxy)) 347 | 348 | Trace-EnteringInvocation -InvocationInfo $MyInvocation 349 | $originalErrorActionPreference = $ErrorActionPreference 350 | try { 351 | $ErrorActionPreference = 'Stop' 352 | 353 | # Default the URI to the collection URI. 354 | if (!$Uri) { 355 | $Uri = Get-TaskVariable -Name System.TeamFoundationCollectionUri -Require 356 | } 357 | 358 | # Cast the URI. 359 | [uri]$Uri = New-Object System.Uri($Uri) 360 | 361 | # Default the credentials. 362 | if (!$VssCredentials) { 363 | $VssCredentials = Get-VssCredentials -OMDirectory $OMDirectory 364 | } 365 | 366 | # Validate the type can be loaded. 367 | $null = Get-OMType -TypeName $TypeName -OMKind 'WebApi' -OMDirectory $OMDirectory -Require 368 | 369 | # Update proxy setting for vss http client 370 | [Microsoft.VisualStudio.Services.Common.VssHttpMessageHandler]::DefaultWebProxy = $WebProxy 371 | 372 | # Try to construct the HTTP client. 373 | Write-Verbose "Constructing HTTP client." 374 | try { 375 | return New-Object $TypeName($Uri, $VssCredentials) 376 | } catch { 377 | # Rethrow if the exception is not due to Newtonsoft.Json DLL not found. 378 | if ($_.Exception.InnerException -isnot [System.IO.FileNotFoundException] -or 379 | $_.Exception.InnerException.FileName -notlike 'Newtonsoft.Json, *') { 380 | 381 | throw 382 | } 383 | 384 | # Default the OMDirectory to the directory of the entry script for the task. 385 | if (!$OMDirectory) { 386 | $OMDirectory = [System.IO.Path]::GetFullPath("$PSScriptRoot\..\..") 387 | Write-Verbose "Defaulted OM directory to: '$OMDirectory'" 388 | } 389 | 390 | # Test if the Newtonsoft.Json DLL exists in the OM directory. 391 | $newtonsoftDll = [System.IO.Path]::Combine($OMDirectory, "Newtonsoft.Json.dll") 392 | Write-Verbose "Testing file path: '$newtonsoftDll'" 393 | if (!(Test-Path -LiteralPath $newtonsoftDll -PathType Leaf)) { 394 | Write-Verbose 'Not found. Rethrowing exception.' 395 | throw 396 | } 397 | 398 | # Add a binding redirect and try again. Parts of the Dev15 preview SDK have a 399 | # dependency on the 6.0.0.0 Newtonsoft.Json DLL, while other parts reference 400 | # the 8.0.0.0 Newtonsoft.Json DLL. 401 | Write-Verbose "Adding assembly resolver." 402 | $onAssemblyResolve = [System.ResolveEventHandler]{ 403 | param($sender, $e) 404 | 405 | if ($e.Name -like 'Newtonsoft.Json, *') { 406 | Write-Verbose "Resolving '$($e.Name)'" 407 | return [System.Reflection.Assembly]::LoadFrom($newtonsoftDll) 408 | } 409 | 410 | Write-Verbose "Unable to resolve assembly name '$($e.Name)'" 411 | return $null 412 | } 413 | [System.AppDomain]::CurrentDomain.add_AssemblyResolve($onAssemblyResolve) 414 | try { 415 | # Try again to construct the HTTP client. 416 | Write-Verbose "Trying again to construct the HTTP client." 417 | return New-Object $TypeName($Uri, $VssCredentials) 418 | } finally { 419 | # Unregister the assembly resolver. 420 | Write-Verbose "Removing assemlby resolver." 421 | [System.AppDomain]::CurrentDomain.remove_AssemblyResolve($onAssemblyResolve) 422 | } 423 | } 424 | } catch { 425 | $ErrorActionPreference = $originalErrorActionPreference 426 | Write-Error $_ 427 | } finally { 428 | Trace-LeavingInvocation -InvocationInfo $MyInvocation 429 | } 430 | } 431 | 432 | <# 433 | .SYNOPSIS 434 | Gets a VstsTaskSdk.VstsWebProxy 435 | 436 | .DESCRIPTION 437 | Gets an instance of a VstsTaskSdk.VstsWebProxy that has same proxy configuration as Build/Release agent. 438 | 439 | VstsTaskSdk.VstsWebProxy implement System.Net.IWebProxy interface. 440 | 441 | .EXAMPLE 442 | $webProxy = Get-VstsWebProxy 443 | $webProxy.GetProxy(New-Object System.Uri("https://github.com/Microsoft/vsts-task-lib")) 444 | #> 445 | function Get-WebProxy { 446 | [CmdletBinding()] 447 | param() 448 | 449 | Trace-EnteringInvocation -InvocationInfo $MyInvocation 450 | try 451 | { 452 | # Min agent version that supports proxy 453 | Assert-Agent -Minimum '2.105.7' 454 | 455 | $proxyUrl = Get-TaskVariable -Name Agent.ProxyUrl 456 | $proxyUserName = Get-TaskVariable -Name Agent.ProxyUserName 457 | $proxyPassword = Get-TaskVariable -Name Agent.ProxyPassword 458 | $proxyBypassListJson = Get-TaskVariable -Name Agent.ProxyBypassList 459 | [string[]]$ProxyBypassList = ConvertFrom-Json -InputObject $ProxyBypassListJson 460 | 461 | return New-Object -TypeName VstsTaskSdk.VstsWebProxy -ArgumentList @($proxyUrl, $proxyUserName, $proxyPassword, $proxyBypassList) 462 | } 463 | finally { 464 | Trace-LeavingInvocation -InvocationInfo $MyInvocation 465 | } 466 | } 467 | 468 | ######################################## 469 | # Private functions. 470 | ######################################## 471 | function Get-OMType { 472 | [CmdletBinding()] 473 | param( 474 | [Parameter(Mandatory = $true)] 475 | [string]$TypeName, 476 | 477 | [ValidateSet('ExtendedClient', 'WebApi')] 478 | [Parameter(Mandatory = $true)] 479 | [string]$OMKind, 480 | 481 | [string]$OMDirectory, 482 | 483 | [switch]$Require) 484 | 485 | Trace-EnteringInvocation -InvocationInfo $MyInvocation 486 | try { 487 | # Default the OMDirectory to the directory of the entry script for the task. 488 | if (!$OMDirectory) { 489 | $OMDirectory = [System.IO.Path]::GetFullPath("$PSScriptRoot\..\..") 490 | Write-Verbose "Defaulted OM directory to: '$OMDirectory'" 491 | } 492 | 493 | # Try to load the type. 494 | $errorRecord = $null 495 | Write-Verbose "Testing whether type can be loaded: '$TypeName'" 496 | $ErrorActionPreference = 'Ignore' 497 | try { 498 | # Failure when attempting to cast a string to a type, transfers control to the 499 | # catch handler even when the error action preference is ignore. The error action 500 | # is set to Ignore so the $Error variable is not polluted. 501 | $type = [type]$TypeName 502 | 503 | # Success. 504 | Write-Verbose "The type was loaded successfully." 505 | return $type 506 | } catch { 507 | # Store the error record. 508 | $errorRecord = $_ 509 | } 510 | 511 | $ErrorActionPreference = 'Stop' 512 | Write-Verbose "The type was not loaded." 513 | 514 | # Build a list of candidate DLL file paths from the namespace. 515 | $dllPaths = @( ) 516 | $namespace = $TypeName 517 | while ($namespace.LastIndexOf('.') -gt 0) { 518 | # Trim the next segment from the namespace. 519 | $namespace = $namespace.SubString(0, $namespace.LastIndexOf('.')) 520 | 521 | # Derive potential DLL file paths based on the namespace and OM kind (i.e. extended client vs web API). 522 | if ($OMKind -eq 'ExtendedClient') { 523 | if ($namespace -like 'Microsoft.TeamFoundation.*') { 524 | $dllPaths += [System.IO.Path]::Combine($OMDirectory, "$namespace.dll") 525 | } 526 | } else { 527 | if ($namespace -like 'Microsoft.TeamFoundation.*' -or 528 | $namespace -like 'Microsoft.VisualStudio.Services' -or 529 | $namespace -like 'Microsoft.VisualStudio.Services.*') { 530 | 531 | $dllPaths += [System.IO.Path]::Combine($OMDirectory, "$namespace.WebApi.dll") 532 | $dllPaths += [System.IO.Path]::Combine($OMDirectory, "$namespace.dll") 533 | } 534 | } 535 | } 536 | 537 | foreach ($dllPath in $dllPaths) { 538 | # Check whether the DLL exists. 539 | Write-Verbose "Testing leaf path: '$dllPath'" 540 | if (!(Test-Path -PathType Leaf -LiteralPath "$dllPath")) { 541 | Write-Verbose "Not found." 542 | continue 543 | } 544 | 545 | # Load the DLL. 546 | Write-Verbose "Loading assembly: $dllPath" 547 | try { 548 | Add-Type -LiteralPath $dllPath 549 | } catch { 550 | # Write the information to the verbose stream and proceed to attempt to load the requested type. 551 | # 552 | # The requested type may successfully load now. For example, the type used with the 14.0 Web API for the 553 | # federated credential (VssOAuthCredential) resides in Microsoft.VisualStudio.Services.Client.dll. Even 554 | # though loading the DLL results in a ReflectionTypeLoadException when Microsoft.ServiceBus.dll (approx 3.75mb) 555 | # is not present, enough types are loaded to use the VssOAuthCredential federated credential with the Web API 556 | # HTTP clients. 557 | Write-Verbose "$($_.Exception.GetType().FullName): $($_.Exception.Message)" 558 | if ($_.Exception -is [System.Reflection.ReflectionTypeLoadException]) { 559 | for ($i = 0 ; $i -lt $_.Exception.LoaderExceptions.Length ; $i++) { 560 | $loaderException = $_.Exception.LoaderExceptions[$i] 561 | Write-Verbose "LoaderExceptions[$i]: $($loaderException.GetType().FullName): $($loaderException.Message)" 562 | } 563 | } 564 | } 565 | 566 | # Try to load the type. 567 | Write-Verbose "Testing whether type can be loaded: '$TypeName'" 568 | $ErrorActionPreference = 'Ignore' 569 | try { 570 | # Failure when attempting to cast a string to a type, transfers control to the 571 | # catch handler even when the error action preference is ignore. The error action 572 | # is set to Ignore so the $Error variable is not polluted. 573 | $type = [type]$TypeName 574 | 575 | # Success. 576 | Write-Verbose "The type was loaded successfully." 577 | return $type 578 | } catch { 579 | $errorRecord = $_ 580 | } 581 | 582 | $ErrorActionPreference = 'Stop' 583 | Write-Verbose "The type was not loaded." 584 | } 585 | 586 | # Check whether to propagate the error. 587 | if ($Require) { 588 | Write-Error $errorRecord 589 | } 590 | } finally { 591 | Trace-LeavingInvocation -InvocationInfo $MyInvocation 592 | } 593 | } 594 | -------------------------------------------------------------------------------- /hugo-task/ps_modules/VstsTaskSdk/FindFunctions.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Finds files using match patterns. 4 | 5 | .DESCRIPTION 6 | Determines the find root from a list of patterns. Performs the find and then applies the glob patterns. Supports interleaved exclude patterns. Unrooted patterns are rooted using defaultRoot, unless matchOptions.matchBase is specified and the pattern is a basename only. For matchBase cases, the defaultRoot is used as the find root. 7 | 8 | .PARAMETER DefaultRoot 9 | Default path to root unrooted patterns. Falls back to System.DefaultWorkingDirectory or current location. 10 | 11 | .PARAMETER Pattern 12 | Patterns to apply. Supports interleaved exclude patterns. 13 | 14 | .PARAMETER FindOptions 15 | When the FindOptions parameter is not specified, defaults to (New-VstsFindOptions -FollowSymbolicLinksTrue). Following soft links is generally appropriate unless deleting files. 16 | 17 | .PARAMETER MatchOptions 18 | When the MatchOptions parameter is not specified, defaults to (New-VstsMatchOptions -Dot -NoBrace -NoCase). 19 | #> 20 | function Find-Match { 21 | [CmdletBinding()] 22 | param( 23 | [Parameter()] 24 | [string]$DefaultRoot, 25 | [Parameter()] 26 | [string[]]$Pattern, 27 | $FindOptions, 28 | $MatchOptions) 29 | 30 | Trace-EnteringInvocation $MyInvocation -Parameter None 31 | $originalErrorActionPreference = $ErrorActionPreference 32 | try { 33 | $ErrorActionPreference = 'Stop' 34 | 35 | # Apply defaults for parameters and trace. 36 | if (!$DefaultRoot) { 37 | $DefaultRoot = Get-TaskVariable -Name 'System.DefaultWorkingDirectory' -Default (Get-Location).Path 38 | } 39 | 40 | Write-Verbose "DefaultRoot: '$DefaultRoot'" 41 | if (!$FindOptions) { 42 | $FindOptions = New-FindOptions -FollowSpecifiedSymbolicLink -FollowSymbolicLinks 43 | } 44 | 45 | Trace-FindOptions -Options $FindOptions 46 | if (!$MatchOptions) { 47 | $MatchOptions = New-MatchOptions -Dot -NoBrace -NoCase 48 | } 49 | 50 | Trace-MatchOptions -Options $MatchOptions 51 | Add-Type -LiteralPath $PSScriptRoot\Minimatch.dll 52 | 53 | # Normalize slashes for root dir. 54 | $DefaultRoot = ConvertTo-NormalizedSeparators -Path $DefaultRoot 55 | 56 | $results = @{ } 57 | $originalMatchOptions = $MatchOptions 58 | foreach ($pat in $Pattern) { 59 | Write-Verbose "Pattern: '$pat'" 60 | 61 | # Trim and skip empty. 62 | $pat = "$pat".Trim() 63 | if (!$pat) { 64 | Write-Verbose 'Skipping empty pattern.' 65 | continue 66 | } 67 | 68 | # Clone match options. 69 | $MatchOptions = Copy-MatchOptions -Options $originalMatchOptions 70 | 71 | # Skip comments. 72 | if (!$MatchOptions.NoComment -and $pat.StartsWith('#')) { 73 | Write-Verbose 'Skipping comment.' 74 | continue 75 | } 76 | 77 | # Set NoComment. Brace expansion could result in a leading '#'. 78 | $MatchOptions.NoComment = $true 79 | 80 | # Determine whether pattern is include or exclude. 81 | $negateCount = 0 82 | if (!$MatchOptions.NoNegate) { 83 | while ($negateCount -lt $pat.Length -and $pat[$negateCount] -eq '!') { 84 | $negateCount++ 85 | } 86 | 87 | $pat = $pat.Substring($negateCount) # trim leading '!' 88 | if ($negateCount) { 89 | Write-Verbose "Trimmed leading '!'. Pattern: '$pat'" 90 | } 91 | } 92 | 93 | $isIncludePattern = $negateCount -eq 0 -or 94 | ($negateCount % 2 -eq 0 -and !$MatchOptions.FlipNegate) -or 95 | ($negateCount % 2 -eq 1 -and $MatchOptions.FlipNegate) 96 | 97 | # Set NoNegate. Brace expansion could result in a leading '!'. 98 | $MatchOptions.NoNegate = $true 99 | $MatchOptions.FlipNegate = $false 100 | 101 | # Trim and skip empty. 102 | $pat = "$pat".Trim() 103 | if (!$pat) { 104 | Write-Verbose 'Skipping empty pattern.' 105 | continue 106 | } 107 | 108 | # Expand braces - required to accurately interpret findPath. 109 | $expanded = $null 110 | $preExpanded = $pat 111 | if ($MatchOptions.NoBrace) { 112 | $expanded = @( $pat ) 113 | } else { 114 | # Convert slashes on Windows before calling braceExpand(). Unfortunately this means braces cannot 115 | # be escaped on Windows, this limitation is consistent with current limitations of minimatch (3.0.3). 116 | Write-Verbose "Expanding braces." 117 | $convertedPattern = $pat -replace '\\', '/' 118 | $expanded = [Minimatch.Minimatcher]::BraceExpand( 119 | $convertedPattern, 120 | (ConvertTo-MinimatchOptions -Options $MatchOptions)) 121 | } 122 | 123 | # Set NoBrace. 124 | $MatchOptions.NoBrace = $true 125 | 126 | foreach ($pat in $expanded) { 127 | if ($pat -ne $preExpanded) { 128 | Write-Verbose "Pattern: '$pat'" 129 | } 130 | 131 | # Trim and skip empty. 132 | $pat = "$pat".Trim() 133 | if (!$pat) { 134 | Write-Verbose "Skipping empty pattern." 135 | continue 136 | } 137 | 138 | if ($isIncludePattern) { 139 | # Determine the findPath. 140 | $findInfo = Get-FindInfoFromPattern -DefaultRoot $DefaultRoot -Pattern $pat -MatchOptions $MatchOptions 141 | $findPath = $findInfo.FindPath 142 | Write-Verbose "FindPath: '$findPath'" 143 | 144 | if (!$findPath) { 145 | Write-Verbose "Skipping empty path." 146 | continue 147 | } 148 | 149 | # Perform the find. 150 | Write-Verbose "StatOnly: '$($findInfo.StatOnly)'" 151 | [string[]]$findResults = @( ) 152 | if ($findInfo.StatOnly) { 153 | # Simply stat the path - all path segments were used to build the path. 154 | if ((Test-Path -LiteralPath $findPath)) { 155 | $findResults += $findPath 156 | } 157 | } else { 158 | $findResults = Get-FindResult -Path $findPath -Options $FindOptions 159 | } 160 | 161 | Write-Verbose "Found $($findResults.Count) paths." 162 | 163 | # Apply the pattern. 164 | Write-Verbose "Applying include pattern." 165 | if ($findInfo.AdjustedPattern -ne $pat) { 166 | Write-Verbose "AdjustedPattern: '$($findInfo.AdjustedPattern)'" 167 | $pat = $findInfo.AdjustedPattern 168 | } 169 | 170 | $matchResults = [Minimatch.Minimatcher]::Filter( 171 | $findResults, 172 | $pat, 173 | (ConvertTo-MinimatchOptions -Options $MatchOptions)) 174 | 175 | # Union the results. 176 | $matchCount = 0 177 | foreach ($matchResult in $matchResults) { 178 | $matchCount++ 179 | $results[$matchResult.ToUpperInvariant()] = $matchResult 180 | } 181 | 182 | Write-Verbose "$matchCount matches" 183 | } else { 184 | # Check if basename only and MatchBase=true. 185 | if ($MatchOptions.MatchBase -and 186 | !(Test-Rooted -Path $pat) -and 187 | ($pat -replace '\\', '/').IndexOf('/') -lt 0) { 188 | 189 | # Do not root the pattern. 190 | Write-Verbose "MatchBase and basename only." 191 | } else { 192 | # Root the exclude pattern. 193 | $pat = Get-RootedPattern -DefaultRoot $DefaultRoot -Pattern $pat 194 | Write-Verbose "After Get-RootedPattern, pattern: '$pat'" 195 | } 196 | 197 | # Apply the pattern. 198 | Write-Verbose 'Applying exclude pattern.' 199 | $matchResults = [Minimatch.Minimatcher]::Filter( 200 | [string[]]$results.Values, 201 | $pat, 202 | (ConvertTo-MinimatchOptions -Options $MatchOptions)) 203 | 204 | # Subtract the results. 205 | $matchCount = 0 206 | foreach ($matchResult in $matchResults) { 207 | $matchCount++ 208 | $results.Remove($matchResult.ToUpperInvariant()) 209 | } 210 | 211 | Write-Verbose "$matchCount matches" 212 | } 213 | } 214 | } 215 | 216 | $finalResult = @( $results.Values | Sort-Object ) 217 | Write-Verbose "$($finalResult.Count) final results" 218 | return $finalResult 219 | } catch { 220 | $ErrorActionPreference = $originalErrorActionPreference 221 | Write-Error $_ 222 | } finally { 223 | Trace-LeavingInvocation -InvocationInfo $MyInvocation 224 | } 225 | } 226 | 227 | <# 228 | .SYNOPSIS 229 | Creates FindOptions for use with Find-VstsMatch. 230 | 231 | .DESCRIPTION 232 | Creates FindOptions for use with Find-VstsMatch. Contains switches to control whether to follow symlinks. 233 | 234 | .PARAMETER FollowSpecifiedSymbolicLink 235 | Indicates whether to traverse descendants if the specified path is a symbolic link directory. Does not cause nested symbolic link directories to be traversed. 236 | 237 | .PARAMETER FollowSymbolicLinks 238 | Indicates whether to traverse descendants of symbolic link directories. 239 | #> 240 | function New-FindOptions { 241 | [CmdletBinding()] 242 | param( 243 | [switch]$FollowSpecifiedSymbolicLink, 244 | [switch]$FollowSymbolicLinks) 245 | 246 | return New-Object psobject -Property @{ 247 | FollowSpecifiedSymbolicLink = $FollowSpecifiedSymbolicLink.IsPresent 248 | FollowSymbolicLinks = $FollowSymbolicLinks.IsPresent 249 | } 250 | } 251 | 252 | <# 253 | .SYNOPSIS 254 | Creates MatchOptions for use with Find-VstsMatch and Select-VstsMatch. 255 | 256 | .DESCRIPTION 257 | Creates MatchOptions for use with Find-VstsMatch and Select-VstsMatch. Contains switches to control which pattern matching options are applied. 258 | #> 259 | function New-MatchOptions { 260 | [CmdletBinding()] 261 | param( 262 | [switch]$Dot, 263 | [switch]$FlipNegate, 264 | [switch]$MatchBase, 265 | [switch]$NoBrace, 266 | [switch]$NoCase, 267 | [switch]$NoComment, 268 | [switch]$NoExt, 269 | [switch]$NoGlobStar, 270 | [switch]$NoNegate, 271 | [switch]$NoNull) 272 | 273 | return New-Object psobject -Property @{ 274 | Dot = $Dot.IsPresent 275 | FlipNegate = $FlipNegate.IsPresent 276 | MatchBase = $MatchBase.IsPresent 277 | NoBrace = $NoBrace.IsPresent 278 | NoCase = $NoCase.IsPresent 279 | NoComment = $NoComment.IsPresent 280 | NoExt = $NoExt.IsPresent 281 | NoGlobStar = $NoGlobStar.IsPresent 282 | NoNegate = $NoNegate.IsPresent 283 | NoNull = $NoNull.IsPresent 284 | } 285 | } 286 | 287 | <# 288 | .SYNOPSIS 289 | Applies match patterns against a list of files. 290 | 291 | .DESCRIPTION 292 | Applies match patterns to a list of paths. Supports interleaved exclude patterns. 293 | 294 | .PARAMETER ItemPath 295 | Array of paths. 296 | 297 | .PARAMETER Pattern 298 | Patterns to apply. Supports interleaved exclude patterns. 299 | 300 | .PARAMETER PatternRoot 301 | Default root to apply to unrooted patterns. Not applied to basename-only patterns when Options.MatchBase is true. 302 | 303 | .PARAMETER Options 304 | When the Options parameter is not specified, defaults to (New-VstsMatchOptions -Dot -NoBrace -NoCase). 305 | #> 306 | function Select-Match { 307 | [CmdletBinding()] 308 | param( 309 | [Parameter()] 310 | [string[]]$ItemPath, 311 | [Parameter()] 312 | [string[]]$Pattern, 313 | [Parameter()] 314 | [string]$PatternRoot, 315 | $Options) 316 | 317 | 318 | Trace-EnteringInvocation $MyInvocation -Parameter None 319 | $originalErrorActionPreference = $ErrorActionPreference 320 | try { 321 | $ErrorActionPreference = 'Stop' 322 | if (!$Options) { 323 | $Options = New-MatchOptions -Dot -NoBrace -NoCase 324 | } 325 | 326 | Trace-MatchOptions -Options $Options 327 | Add-Type -LiteralPath $PSScriptRoot\Minimatch.dll 328 | 329 | # Hashtable to keep track of matches. 330 | $map = @{ } 331 | 332 | $originalOptions = $Options 333 | foreach ($pat in $Pattern) { 334 | Write-Verbose "Pattern: '$pat'" 335 | 336 | # Trim and skip empty. 337 | $pat = "$pat".Trim() 338 | if (!$pat) { 339 | Write-Verbose 'Skipping empty pattern.' 340 | continue 341 | } 342 | 343 | # Clone match options. 344 | $Options = Copy-MatchOptions -Options $originalOptions 345 | 346 | # Skip comments. 347 | if (!$Options.NoComment -and $pat.StartsWith('#')) { 348 | Write-Verbose 'Skipping comment.' 349 | continue 350 | } 351 | 352 | # Set NoComment. Brace expansion could result in a leading '#'. 353 | $Options.NoComment = $true 354 | 355 | # Determine whether pattern is include or exclude. 356 | $negateCount = 0 357 | if (!$Options.NoNegate) { 358 | while ($negateCount -lt $pat.Length -and $pat[$negateCount] -eq '!') { 359 | $negateCount++ 360 | } 361 | 362 | $pat = $pat.Substring($negateCount) # trim leading '!' 363 | if ($negateCount) { 364 | Write-Verbose "Trimmed leading '!'. Pattern: '$pat'" 365 | } 366 | } 367 | 368 | $isIncludePattern = $negateCount -eq 0 -or 369 | ($negateCount % 2 -eq 0 -and !$Options.FlipNegate) -or 370 | ($negateCount % 2 -eq 1 -and $Options.FlipNegate) 371 | 372 | # Set NoNegate. Brace expansion could result in a leading '!'. 373 | $Options.NoNegate = $true 374 | $Options.FlipNegate = $false 375 | 376 | # Expand braces - required to accurately root patterns. 377 | $expanded = $null 378 | $preExpanded = $pat 379 | if ($Options.NoBrace) { 380 | $expanded = @( $pat ) 381 | } else { 382 | # Convert slashes on Windows before calling braceExpand(). Unfortunately this means braces cannot 383 | # be escaped on Windows, this limitation is consistent with current limitations of minimatch (3.0.3). 384 | Write-Verbose "Expanding braces." 385 | $convertedPattern = $pat -replace '\\', '/' 386 | $expanded = [Minimatch.Minimatcher]::BraceExpand( 387 | $convertedPattern, 388 | (ConvertTo-MinimatchOptions -Options $Options)) 389 | } 390 | 391 | # Set NoBrace. 392 | $Options.NoBrace = $true 393 | 394 | foreach ($pat in $expanded) { 395 | if ($pat -ne $preExpanded) { 396 | Write-Verbose "Pattern: '$pat'" 397 | } 398 | 399 | # Trim and skip empty. 400 | $pat = "$pat".Trim() 401 | if (!$pat) { 402 | Write-Verbose "Skipping empty pattern." 403 | continue 404 | } 405 | 406 | # Root the pattern when all of the following conditions are true: 407 | if ($PatternRoot -and # PatternRoot is supplied 408 | !(Test-Rooted -Path $pat) -and # AND pattern is not rooted 409 | # # AND MatchBase=false or not basename only 410 | (!$Options.MatchBase -or ($pat -replace '\\', '/').IndexOf('/') -ge 0)) { 411 | 412 | # Root the include pattern. 413 | $pat = Get-RootedPattern -DefaultRoot $PatternRoot -Pattern $pat 414 | Write-Verbose "After Get-RootedPattern, pattern: '$pat'" 415 | } 416 | 417 | if ($isIncludePattern) { 418 | # Apply the pattern. 419 | Write-Verbose 'Applying include pattern against original list.' 420 | $matchResults = [Minimatch.Minimatcher]::Filter( 421 | $ItemPath, 422 | $pat, 423 | (ConvertTo-MinimatchOptions -Options $Options)) 424 | 425 | # Union the results. 426 | $matchCount = 0 427 | foreach ($matchResult in $matchResults) { 428 | $matchCount++ 429 | $map[$matchResult] = $true 430 | } 431 | 432 | Write-Verbose "$matchCount matches" 433 | } else { 434 | # Apply the pattern. 435 | Write-Verbose 'Applying exclude pattern against original list' 436 | $matchResults = [Minimatch.Minimatcher]::Filter( 437 | $ItemPath, 438 | $pat, 439 | (ConvertTo-MinimatchOptions -Options $Options)) 440 | 441 | # Subtract the results. 442 | $matchCount = 0 443 | foreach ($matchResult in $matchResults) { 444 | $matchCount++ 445 | $map.Remove($matchResult) 446 | } 447 | 448 | Write-Verbose "$matchCount matches" 449 | } 450 | } 451 | } 452 | 453 | # return a filtered version of the original list (preserves order and prevents duplication) 454 | $result = $ItemPath | Where-Object { $map[$_] } 455 | Write-Verbose "$($result.Count) final results" 456 | $result 457 | } catch { 458 | $ErrorActionPreference = $originalErrorActionPreference 459 | Write-Error $_ 460 | } finally { 461 | Trace-LeavingInvocation -InvocationInfo $MyInvocation 462 | } 463 | } 464 | 465 | ################################################################################ 466 | # Private functions. 467 | ################################################################################ 468 | 469 | function Copy-MatchOptions { 470 | [CmdletBinding()] 471 | param($Options) 472 | 473 | return New-Object psobject -Property @{ 474 | Dot = $Options.Dot -eq $true 475 | FlipNegate = $Options.FlipNegate -eq $true 476 | MatchBase = $Options.MatchBase -eq $true 477 | NoBrace = $Options.NoBrace -eq $true 478 | NoCase = $Options.NoCase -eq $true 479 | NoComment = $Options.NoComment -eq $true 480 | NoExt = $Options.NoExt -eq $true 481 | NoGlobStar = $Options.NoGlobStar -eq $true 482 | NoNegate = $Options.NoNegate -eq $true 483 | NoNull = $Options.NoNull -eq $true 484 | } 485 | } 486 | 487 | function ConvertTo-MinimatchOptions { 488 | [CmdletBinding()] 489 | param($Options) 490 | 491 | $opt = New-Object Minimatch.Options 492 | $opt.AllowWindowsPaths = $true 493 | $opt.Dot = $Options.Dot -eq $true 494 | $opt.FlipNegate = $Options.FlipNegate -eq $true 495 | $opt.MatchBase = $Options.MatchBase -eq $true 496 | $opt.NoBrace = $Options.NoBrace -eq $true 497 | $opt.NoCase = $Options.NoCase -eq $true 498 | $opt.NoComment = $Options.NoComment -eq $true 499 | $opt.NoExt = $Options.NoExt -eq $true 500 | $opt.NoGlobStar = $Options.NoGlobStar -eq $true 501 | $opt.NoNegate = $Options.NoNegate -eq $true 502 | $opt.NoNull = $Options.NoNull -eq $true 503 | return $opt 504 | } 505 | 506 | function ConvertTo-NormalizedSeparators { 507 | [CmdletBinding()] 508 | param([string]$Path) 509 | 510 | # Convert slashes. 511 | $Path = "$Path".Replace('/', '\') 512 | 513 | # Remove redundant slashes. 514 | $isUnc = $Path -match '^\\\\+[^\\]' 515 | $Path = $Path -replace '\\\\+', '\' 516 | if ($isUnc) { 517 | $Path = '\' + $Path 518 | } 519 | 520 | return $Path 521 | } 522 | 523 | function Get-FindInfoFromPattern { 524 | [CmdletBinding()] 525 | param( 526 | [Parameter(Mandatory = $true)] 527 | [string]$DefaultRoot, 528 | [Parameter(Mandatory = $true)] 529 | [string]$Pattern, 530 | [Parameter(Mandatory = $true)] 531 | $MatchOptions) 532 | 533 | if (!$MatchOptions.NoBrace) { 534 | throw "Get-FindInfoFromPattern expected MatchOptions.NoBrace to be true." 535 | } 536 | 537 | # For the sake of determining the find path, pretend NoCase=false. 538 | $MatchOptions = Copy-MatchOptions -Options $MatchOptions 539 | $MatchOptions.NoCase = $false 540 | 541 | # Check if basename only and MatchBase=true 542 | if ($MatchOptions.MatchBase -and 543 | !(Test-Rooted -Path $Pattern) -and 544 | ($Pattern -replace '\\', '/').IndexOf('/') -lt 0) { 545 | 546 | return New-Object psobject -Property @{ 547 | AdjustedPattern = $Pattern 548 | FindPath = $DefaultRoot 549 | StatOnly = $false 550 | } 551 | } 552 | 553 | # The technique applied by this function is to use the information on the Minimatch object determine 554 | # the findPath. Minimatch breaks the pattern into path segments, and exposes information about which 555 | # segments are literal vs patterns. 556 | # 557 | # Note, the technique currently imposes a limitation for drive-relative paths with a glob in the 558 | # first segment, e.g. C:hello*/world. It's feasible to overcome this limitation, but is left unsolved 559 | # for now. 560 | $minimatchObj = New-Object Minimatch.Minimatcher($Pattern, (ConvertTo-MinimatchOptions -Options $MatchOptions)) 561 | 562 | # The "set" field is a two-dimensional enumerable of parsed path segment info. The outer enumerable should only 563 | # contain one item, otherwise something went wrong. Brace expansion can result in multiple items in the outer 564 | # enumerable, but that should be turned off by the time this function is reached. 565 | # 566 | # Note, "set" is a private field in the .NET implementation but is documented as a feature in the nodejs 567 | # implementation. The .NET implementation is a port and is by a different author. 568 | $setFieldInfo = $minimatchObj.GetType().GetField('set', 'Instance,NonPublic') 569 | [object[]]$set = $setFieldInfo.GetValue($minimatchObj) 570 | if ($set.Count -ne 1) { 571 | throw "Get-FindInfoFromPattern expected Minimatch.Minimatcher(...).set.Count to be 1. Actual: '$($set.Count)'" 572 | } 573 | 574 | [string[]]$literalSegments = @( ) 575 | [object[]]$parsedSegments = $set[0] 576 | foreach ($parsedSegment in $parsedSegments) { 577 | if ($parsedSegment.GetType().Name -eq 'LiteralItem') { 578 | # The item is a LiteralItem when the original input for the path segment does not contain any 579 | # unescaped glob characters. 580 | $literalSegments += $parsedSegment.Source; 581 | continue 582 | } 583 | 584 | break; 585 | } 586 | 587 | # Join the literal segments back together. Minimatch converts '\' to '/' on Windows, then squashes 588 | # consequetive slashes, and finally splits on slash. This means that UNC format is lost, but can 589 | # be detected from the original pattern. 590 | $joinedSegments = [string]::Join('/', $literalSegments) 591 | if ($joinedSegments -and ($Pattern -replace '\\', '/').StartsWith('//')) { 592 | $joinedSegments = '/' + $joinedSegments # restore UNC format 593 | } 594 | 595 | # Determine the find path. 596 | $findPath = '' 597 | if ((Test-Rooted -Path $Pattern)) { # The pattern is rooted. 598 | $findPath = $joinedSegments 599 | } elseif ($joinedSegments) { # The pattern is not rooted, and literal segements were found. 600 | $findPath = [System.IO.Path]::Combine($DefaultRoot, $joinedSegments) 601 | } else { # The pattern is not rooted, and no literal segements were found. 602 | $findPath = $DefaultRoot 603 | } 604 | 605 | # Clean up the path. 606 | if ($findPath) { 607 | $findPath = [System.IO.Path]::GetDirectoryName(([System.IO.Path]::Combine($findPath, '_'))) # Hack to remove unnecessary trailing slash. 608 | $findPath = ConvertTo-NormalizedSeparators -Path $findPath 609 | } 610 | 611 | return New-Object psobject -Property @{ 612 | AdjustedPattern = Get-RootedPattern -DefaultRoot $DefaultRoot -Pattern $Pattern 613 | FindPath = $findPath 614 | StatOnly = $literalSegments.Count -eq $parsedSegments.Count 615 | } 616 | } 617 | 618 | function Get-FindResult { 619 | [CmdletBinding()] 620 | param( 621 | [Parameter(Mandatory = $true)] 622 | [string]$Path, 623 | [Parameter(Mandatory = $true)] 624 | $Options) 625 | 626 | if (!(Test-Path -LiteralPath $Path)) { 627 | Write-Verbose 'Path not found.' 628 | return 629 | } 630 | 631 | $Path = ConvertTo-NormalizedSeparators -Path $Path 632 | 633 | # Push the first item. 634 | [System.Collections.Stack]$stack = New-Object System.Collections.Stack 635 | $stack.Push((Get-Item -LiteralPath $Path)) 636 | 637 | $count = 0 638 | while ($stack.Count) { 639 | # Pop the next item and yield the result. 640 | $item = $stack.Pop() 641 | $count++ 642 | $item.FullName 643 | 644 | # Traverse. 645 | if (($item.Attributes -band 0x00000010) -eq 0x00000010) { # Directory 646 | if (($item.Attributes -band 0x00000400) -ne 0x00000400 -or # ReparsePoint 647 | $Options.FollowSymbolicLinks -or 648 | ($count -eq 1 -and $Options.FollowSpecifiedSymbolicLink)) { 649 | 650 | $childItems = @( Get-DirectoryChildItem -Path $Item.FullName -Force ) 651 | [System.Array]::Reverse($childItems) 652 | foreach ($childItem in $childItems) { 653 | $stack.Push($childItem) 654 | } 655 | } 656 | } 657 | } 658 | } 659 | 660 | function Get-RootedPattern { 661 | [CmdletBinding()] 662 | param( 663 | [Parameter(Mandatory = $true)] 664 | [string]$DefaultRoot, 665 | [Parameter(Mandatory = $true)] 666 | [string]$Pattern) 667 | 668 | if ((Test-Rooted -Path $Pattern)) { 669 | return $Pattern 670 | } 671 | 672 | # Normalize root. 673 | $DefaultRoot = ConvertTo-NormalizedSeparators -Path $DefaultRoot 674 | 675 | # Escape special glob characters. 676 | $DefaultRoot = $DefaultRoot -replace '(\[)(?=[^\/]+\])', '[[]' # Escape '[' when ']' follows within the path segment 677 | $DefaultRoot = $DefaultRoot.Replace('?', '[?]') # Escape '?' 678 | $DefaultRoot = $DefaultRoot.Replace('*', '[*]') # Escape '*' 679 | $DefaultRoot = $DefaultRoot -replace '\+\(', '[+](' # Escape '+(' 680 | $DefaultRoot = $DefaultRoot -replace '@\(', '[@](' # Escape '@(' 681 | $DefaultRoot = $DefaultRoot -replace '!\(', '[!](' # Escape '!(' 682 | 683 | if ($DefaultRoot -like '[A-Z]:') { # e.g. C: 684 | return "$DefaultRoot$Pattern" 685 | } 686 | 687 | # Ensure root ends with a separator. 688 | if (!$DefaultRoot.EndsWith('\')) { 689 | $DefaultRoot = "$DefaultRoot\" 690 | } 691 | 692 | return "$DefaultRoot$Pattern" 693 | } 694 | 695 | function Test-Rooted { 696 | [CmdletBinding()] 697 | param( 698 | [Parameter(Mandatory = $true)] 699 | [string]$Path) 700 | 701 | $Path = ConvertTo-NormalizedSeparators -Path $Path 702 | return $Path.StartsWith('\') -or # e.g. \ or \hello or \\hello 703 | $Path -like '[A-Z]:*' # e.g. C: or C:\hello 704 | } 705 | 706 | function Trace-MatchOptions { 707 | [CmdletBinding()] 708 | param($Options) 709 | 710 | Write-Verbose "MatchOptions.Dot: '$($Options.Dot)'" 711 | Write-Verbose "MatchOptions.FlipNegate: '$($Options.FlipNegate)'" 712 | Write-Verbose "MatchOptions.MatchBase: '$($Options.MatchBase)'" 713 | Write-Verbose "MatchOptions.NoBrace: '$($Options.NoBrace)'" 714 | Write-Verbose "MatchOptions.NoCase: '$($Options.NoCase)'" 715 | Write-Verbose "MatchOptions.NoComment: '$($Options.NoComment)'" 716 | Write-Verbose "MatchOptions.NoExt: '$($Options.NoExt)'" 717 | Write-Verbose "MatchOptions.NoGlobStar: '$($Options.NoGlobStar)'" 718 | Write-Verbose "MatchOptions.NoNegate: '$($Options.NoNegate)'" 719 | Write-Verbose "MatchOptions.NoNull: '$($Options.NoNull)'" 720 | } 721 | 722 | function Trace-FindOptions { 723 | [CmdletBinding()] 724 | param($Options) 725 | 726 | Write-Verbose "FindOptions.FollowSpecifiedSymbolicLink: '$($FindOptions.FollowSpecifiedSymbolicLink)'" 727 | Write-Verbose "FindOptions.FollowSymbolicLinks: '$($FindOptions.FollowSymbolicLinks)'" 728 | } 729 | --------------------------------------------------------------------------------