(1) 安裝 Node.js
a. 請到 https://nodejs.org/en/ 下載 Node.js 安裝, 若已經有 Node.js 環境 , 可以忽略此步驟 (檢查 Node.js 版本, 可以使用 node --version 命令)

(2) 安裝 TFS 延伸套件
請從命令提示執行以下命令安裝 TFS 延伸套件
npm install -g tfx-cli 

(1) 從命令提示 建立一個目錄存放 Extension
md c:\temp\tfs-extension
(2) 使用 VS Code 建立一下的檔案結構

a. Task 目錄 - 存放 Task Script 與內容
(a) icon.png - task 的 icon (32x32)

(b) task.json - task 的 manifest 檔案
(c) task.ps1 - 實際執行的 task PowerShell script
b. icon128.png - TFS extension 列表的 icon (128x128)

c. icon512.png - Extension detail 的 Icon (512x512)

d. overview.md - Extension 詳細介紹的說明 (Markdown格式)

e. vss-extension.json - TFS extension 打包時的延伸檔
a. id: guid 作為 task 的識別
b. name: Task 名稱
c. friendlyName: 好辨識的名稱
d. author: 作者名稱, 作者名稱必須要註冊於 MarketPlace 中
e. category: 顯示於 Task 類別
f. version: 定義版本資訊
g. instanceNameFormat: 初始的 task 名稱
h. inputs: 參數定義
i. execution: 執行方式
{
  "id": "7AE8DD31-8197-4C6E-B3A5-5D2052CFA59F",
  "name": "BackupFilesViaPSSession",
  "friendlyName": "Backup Files via PS Session",
  "description": "Backup files using a Powershell Session.",
  "author": "SamLin",
  "helpMarkDown": "",
  "category": "Utility",
  "visibility": [
    "Build",
    "Release"
  ],
  "demands": [
        "DotNetFramework"
    ],
  "version": {
    "Major": "0",
    "Minor": "2",
    "Patch": "19"
  },
  "minimumAgentVersion": "1.83.0",
  "instanceNameFormat": "Powershell Backup files to $(targetpath)",
  "groups": [],
  "inputs": [
    { 
      "name": "sourcepath", 
      "type": "string", 
      "label": "Source Path", 
      "defaultValue": "", 
      "required": true,
      "helpMarkDown": "A local source path on the remote server."
    },
    { 
      "name": "targetpath", 
      "type": "string", 
      "label": "Target Path", 
      "defaultValue": "", 
      "required": true,
      "helpMarkDown": "A local target path on the remote server."
    },
    { 
      "name": "backupname", 
      "type": "string", 
      "label": "Backup Name", 
      "defaultValue": "", 
      "required": true,
      "helpMarkDown": "A backup name will append with date(yyyyMMdd).zip file name."
    },
    { 
        "name": "server", 
        "type": "string", 
        "label": "Remote Machine", 
        "defaultValue": "", 
        "required": true,
        "helpMarkDown": "FQDN of the remote machine you want to reach (or the IP Address)."
    }
  ],
  "execution": {
    "PowerShell": {
      "target": "$(currentDirectory)\\task.ps1",
      "argumentFormat": "",
      "workingDirectory": "$(currentDirectory)"
    }
  }
}
以下範例為遠端備份檔案 script 請自行修改為需要的 script 內容
param (
    [string]  $sourcepath, # 來源端路徑
    [string]  $targetpath, # 備份目標路徑
    [string]  $backupname, # 備份名稱 (將會以備份名稱_yyyyMMdd.zip 為備份檔案名稱)
    [string]  $server      # 遠端主機名稱 (需支援並開啟 PSRemoting)
)
Write-Host "Entering script task.ps1";
Write-Verbose "[server]           --> [$server]"           -Verbose;
Write-Verbose "[sourcepath]       --> [$sourcepath]"       -Verbose;
Write-Verbose "[targetpath]       --> [$targetpath]"       -Verbose;
Write-Verbose "[backupname]       --> [$backupname]"       -Verbose;
### Validates all paths ###
function ValidatePath ([string]$type, [string]$path) {
    Write-Host "Validating $type Path variable.";
    if (-not $path.EndsWith("\")) { $path = "$path\"; }
    Write-Host "$type Path is $path.";
    return $path;
}
$sourcepath = ValidatePath -type "Source" -path $sourcepath;
$targetpath = ValidatePath -type "Target" -path $targetpath;
[System.Management.Automation.Runspaces.PSSession] $session = $null;
$sc = {
    Add-Type -AssemblyName System.IO.Compression.FileSystem    
    $destination = $args[1] + $args[2] + "_" + $(Get-Date -Format 'yyyyMMdd') + ".zip"
    if(Test-Path $destination)
    {   
        # 備份目的檔案存在, 移除檔案
        Write-Host "Remove destination file..."    
        Remove-Item $destination
    }
    if(-Not(Test-Path $args[1]))
    {
        Write-Host "Create destination folder..."
        New-Item -Path $args[1] -ItemType Directory
    }
      
    if(Test-Path $args[0])
    {
        # 備份壓縮原始檔案, 備份完成後移除原始檔案
        Write-Host "Backup source files to destination with [$($args[2])_$(Get-Date -Format 'yyyyMMdd').zip] name..."
        [IO.Compression.ZipFile]::CreateFromDirectory($args[0], $destination)
        Write-Host "Delete source files..."
        Remove-Item $args[0] -Recurse -Confirm:$false
    }
}
Try 
{
    $session = New-PSSession -ComputerName $server    
    Write-Host "Backing up files via remote session.";    
    Invoke-Command -Session $session -ScriptBlock $sc -ArgumentList $sourcepath, $targetpath, $backupname
}
Catch 
{
    Write-Host;
    Write-Error $_;
}
Finally 
{
    Write-Host "Closing Powershell remote session on $server.";
    if ($null -ne $session) { $session | Disconnect-PSSession | Remove-PSSession };
}
Write-Host "Leaving script task.ps1";
a. version: 版本號
b. name: Extension 顯示名稱
c. publisher: 發行者 (需註冊於 MarketPlace)
d. icon: 檔案名稱定義
e. content: 說明檔
f. files: task 檔案路徑
{
    "manifestVersion": 1,
    "id": "backupfilesviapssession-task",
    "version": "0.1.19",
    "name": "Backup Files via PS Session Task",
    "publisher": "SamLin",
    "targets": [
        {
            "id": "Microsoft.VisualStudio.Services"
        }
    ],
    "description": "Backup Files via PS Session Task for TFS",
    "icons": {
        "default": "icon128.png",
        "large": "icon512.png"
    },
    "categories": [
        "Build and release"
    ],
    "tags": [
        "utility",
        "tasks"
    ],
    "screenshots": [],
    "content": {
        "details": {
            "path": "overview.md"
        }
    },
    "links": {
        "getstarted": {
            "uri": "https://www.anisv.com"
        }
	},
    "branding": {
        "color": "black",
        "theme": "dark"
    },
    "galleryFlags": [
        "Public"
    ],
    "scopes": [
        "vso.build_execute", 
        "vso.serviceendpoint_manage" 
    ],
    "files": [
        {
            "path": "Task"
        }
    ],
    "contributions": [
        {
            "id": "backupfilesviapssession-task",
            "type": "ms.vss-distributed-task.task",
            "targets": [
                "ms.vss-distributed-task.tasks"
            ], 
            "properties": { 
                "name": "Task" 
            } 
        }
    ]
}
使用以下的命令將檔案打包
tfx extension create --manifest-globs vss-extension.json
產出檔案格式如下:

a. 請將檔案打包後的檔案 (SamLin.backupfilesviapssession-task-0.1.19.vsix) 複製到 TFS 上
b. 開啟 TFS extension 管理頁面 (http://tfs:8080/tfs/_gallery/manage)
c. 上傳 TFS extension


d. 安裝 Extension 到 Project Collection


(1) 在 Build definition 新增一個 Task, 可以看到 Utility Category 中出現新加入的 Task

(2) 本範例的使用方式:
a. Display name: Task 名稱
b. Source Path: 備份的來源目錄
c. Target Path: 備份的目的目錄
d. Backup Name: 備份名稱
e. Remote Machine: 可以通過 PSRemote 由 Build Agent 連線 PS Remote Session 執行工作的目標主機名稱

(3) 執行結果:

註: 目標主機開啟 PSRemoting 的 PowerShell 語法如下, 其中 TrustedHosts為 Build Agent 所在的主機名稱
Get-Service -Name WinRM
Enable-PSRemoting -Force
winrm s winrm/config/client '@{TrustedHosts="BUILD"}'
winrm quickconfig
Restart-Service -Name WinRM