Create TFS work item with PowerShell - powershell

I'm working on a TFS build with a pre-build PowerShell script that (in addition to building my app) automatically checks out a file where we maintain version, increments the build number, then checks the file in.
I have been able to do this, except that I get an error from the script which results in a partially successful build (orange). I need to have a fully successful (green) build.
Here's the check-in line (using TFS Power Tools for VS 2013):
New-TfsChangeset -Item $versionFile -Override "Automated" -Notes "Code Reviewer=tfs" -Comment "Automated"
The error I receive is that the changeset is not associated with a work item, but the -Override should handle that. The funny thing is that it checks in anyway.
Running locally on my machine instead of the build server, I get the same thing, except that I also see a line that says The policies have been overridden. This tells me that the override is working, but it still outputs the error.
I've tried adding -ErrorAction SilentlyContinue, but it has no effect.
I need one of three options:
A way to suppress output of the checkin error,
A way to create a work item and associate it to the checkin, or
Some other third option that will result in a green build.
Any ideas?

Credit goes to Eddie - MSFT for leading me the right direction, but I wanted to consolidate everything here.
WARNING This will check in all pending changes in the workspace.
Creating a new work item (source)
I did modify it quite a bit to support automation. It connects to TFS and generates a new work item.
function New-WorkItem()
{
# These *should* be registered in the GAC.
# The version numbers will likely have to change as new versions of Visual Studio are installed on the server.
Add-Type -Assembly "Microsoft.TeamFoundation.Client, Version=12.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"
Add-Type -Assembly "Microsoft.TeamFoundation.WorkItemTracking.Client, Version=12.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"
$server = "http://YOURTFSSERVER:8080/tfs"
$tfs = [Microsoft.TeamFoundation.Client.TeamFoundationServerFactory]::GetServer($server)
$type = [Microsoft.TeamFoundation.WorkItemTracking.Client.WorkItemStore]
$store = [Microsoft.TeamFoundation.WorkItemTracking.Client.WorkItemStore] $tfs.GetService($type)
$workItem = New-Object Microsoft.TeamFoundation.WorkItemTracking.Client.WorkItem($store.Projects[0].WorkItemTypes[0])
$workItem.Title = "Automated work item"
$workItem
}
Associating the work item and checking in
Slight modifications to the code from the link given by Eddie, we get the following:
function CheckIn()
{
param([Microsoft.TeamFoundation.WorkItemTracking.Client.WorkItem] $workItem)
$col = Get-TfsCollection("http://YOURTFSSERVER:8080/tfs/YOURCOLLECTION")
$vcs = Get-TfsVersionControlServer($col)
$ws = $vcs.GetWorkspace([System.IO.Path]::GetDirectoryName($anyPathInWorkspace))
$pc = $ws.GetPendingChanges()
$wici = Get-TfsWorkItemCheckinInfo($workItem)
$changeset = $ws.CheckIn($pc, "Automated check in", $null, $wici, $null)
}
That post doesn't tell you that Get-TfsCollection, Get-TfsVersionControlServer, and Get-TfsWorkItemCheckinInfo aren't defined. I had to find them.
I found the first two on http://nkdagility.com/powershell-tfs-2013-api-1-get-tfscollection-and-tfs-services/. I didn't have to change anything.
function Get-TfsCollection
{
param([string] $CollectionUrl)
if ($CollectionUrl -ne "")
{
#if collection is passed then use it and select all projects
$tfs = [Microsoft.TeamFoundation.Client.TfsTeamProjectCollectionFactory]::GetTeamProjectCollection($CollectionUrl)
}
else
{
#if no collection specified, open project picker to select it via gui
$picker = New-Object Microsoft.TeamFoundation.Client.TeamProjectPicker([Microsoft.TeamFoundation.Client.TeamProjectPickerMode]::NoProject, $false)
$dialogResult = $picker.ShowDialog()
if ($dialogResult -ne "OK")
{
#exit
}
$tfs = $picker.SelectedTeamProjectCollection
}
$tfs
}
function Get-TfsVersionControlServer
{
param([Microsoft.TeamFoundation.Client.TfsTeamProjectCollection] $TfsCollection)
$TfsCollection.GetService("Microsoft.TeamFoundation.VersionControl.Client.VersionControlServer")
}
But I couldn't find Get-TfsWorkItemCheckinInfo. The only Google hit was the kinook link from Eddie (and soon probably this answer). Here's what I came up with:
function Get-TfsWorkItemCheckinInfo
{
param([Microsoft.TeamFoundation.WorkItemTracking.Client.WorkItem] $workItem)
$wi = New-Object Microsoft.TeamFoundation.VersionControl.Client.WorkItemCheckinInfo($workItem, [Microsoft.TeamFoundation.VersionControl.Client.WorkItemCheckinAction]::Resolve)
$wi
}
Now we can use it
CheckIn (New-WorkItem)
That's it!

You can create a work item from PowerShell by following this article: http://halanstevens.com/blog/powershell-script-to-create-a-workitem/
Quote the code here for reference:
$key = Get-ItemProperty HKLM:\SOFTWARE\Microsoft\VisualStudio\8.0
$dir = [string] (Get-ItemProperty $key.InstallDir)
$dir += "PrivateAssemblies\"
$lib = $dir + "Microsoft.TeamFoundation.WorkItemTracking.Client.dll"
[Reflection.Assembly]::LoadFrom($lib)
$lib = $dir + "Microsoft.TeamFoundation.Client.dll"
[Reflection.Assembly]::LoadFrom($lib)
"Please enter your Team Foundation Server Name:"
$server = [Console]::ReadLine()
$server = $server.Trim()
"Connecting to " + $server + "..."
$tfs = [Microsoft.TeamFoundation.Client.TeamFoundationServerFactory]::GetServer($server)
$type = [Microsoft.TeamFoundation.WorkItemTracking.Client.WorkItemStore]
$store = [Microsoft.TeamFoundation.WorkItemTracking.Client.WorkItemStore] $tfs.GetService($type)
$workItem = new-object Microsoft.TeamFoundation.WorkItemTracking.Client.WorkItem($store.Projects[0].WorkItemTypes[0])
"Created a new work item of type " + $workItem.Type.Name
$workItem.Title = "Created by Windows PowerShell!"
$workItem.Save()
And then refer to this article to associate the work item to changeset: http://www.kinook.com/Forum/showthread.php?t=4502

Related

Create Receive Location and Send Port in Existing BizTalk Application using Powershell

I am needing to create receive locations and send ports in an already existing BizTalk application using Powershell. I have only seen some documentation on how to create an application but not to call upon one. Any suggestions would be beneficial. There are some things that are commented out, and that is because I cannot disclose that information. I added at the last part on what I have learned of how to create an application, but that is not something that I want for my script. The program below is what I have so far:
#===Create a receive port and location function===#
Function CreateRPandRL ()
{
#Creating Receive Port
$myReceivePort = $catalog.AddNewReceivePort($false)
$myReceivePort.Name = "My Receive Port"
#Creating Receive Location
$myReceiveLocation = $myReceivePort.AddNewReceiveLocation()
foreach ($handler in $catalog.ReceiveHandlers)
{
if ($handler.TransportType.Name -eq "FILE")
{
$myReceiveLocation.ReceiveHandler = $handler
break
}
}
#Associate a transport protocol and file location with receive location
$myReceiveLocation.TransportType = $catalog.ProtocolTypes["FILE"]
$myReceiveLocation.Address = #pick-up file location
#Assign the first receive pipeline found to process the message
foreach ($pipeline in $catalog.Pipelines)
{
if ($pipeline.Type -eq [Microsoft.BizTalk.ExplorerOM.PipelineType] "File_Receive")
{
$myReceiveLocation.ReceivePipeline = $pipeline
break
}
#Enable the receive location
$myReceiveLocation.Enable = $true
}
#Try to commit the changes made so far. If the commit fails, roll back changes
$catalog.SaveChanges()
}
Function CreateSendPorts($Catalog)
{
#=== Register a trap handler to discard changes on exceptions ===#
$ErrorActionPreference="silentlycontinue"
trap { "Exception encountered:`r`n"; $_; "`r`nDiscarding Changes.`r`n";$Catalog.DiscardChanges();exit; }
#=== create a new static one-way send port using FILE transport ===#
$mySendPort = $Catalog.AddNewSendPort($false,$false)
$mySendPort.Name = "My Send Port"
$mySendPort.PrimaryTransport.TransportType = $catalog.ProtocolTypes["FILE"]
$mySendPort.PrimaryTransport.Address = #drop-off file location
$mySendPort.SendPipeline = $Catalog.Pipelines["Microsoft.BizTalk.DefaultPipelines.EdiSend"]
#=== Persist new ports to BizTalk configuration database ===#
Write-Host "Adding $mySendPort.Name..."
$catalog.SaveChanges();
Write-Host "`r`n $mySendPort.Name has been created."
#=== specify filters for content-based routing ===#
Write-Host $mySendPort.Name: Adding a filter
$mySendPort.Filter = "<Filter><Group>" +
"<Statement Property='EDI.ISA06' Operator='0' Value='9999999999'/>" +
"<Statement Property='EDI.ST01' Operator='0' Value='999'/>" +
"<Statement Property='EDI.IsSystemGeneratedAck' Operator='0' Value='true'/>" +
"<Statement Property='BTS.ReceivePortName' Operator='0' Value= $myReceivePort.Name/>" +
"</Group></Filter>"
#=== Persist all changes to BizTalk configuration database ===#
Write-Host $mySendPort.Name + ": Saving changes"
$catalog.SaveChanges();
Write-Host "`r`nFilters for $mySendPort.Name created"
#===========Changing Send Port status===========#
#Register a trap handler to discard changes on exceptions
$ErrorActionPreference="silentlycontinue"
trap { "Exception encountered:`r`n"; $_; "`r`nDiscarding Changes.`r`n";$Catalog.DiscardChanges();exit; }
#start the send port to begin processing messages
$mySendPort.Status = [Microsoft.BizTalk.ExplorerOM.PortStatus] "Started"
Write-Host "Changing" + $mySendPort.Name + "status to ($mySendPort.Status)..."
$catalog.SaveChanges()
Write-Host "Complete."
}
#===Main Script===#
#make sure the ExplorerOM assembly is loaded
[void][System.reflection.Assembly]::LoadwithPartialName("Microsoft.BizTalk.ExplorerOM")
#Connect to the BizTalk management database
$Catalog = New-Object Microsoft.BizTalk.ExplorerOM.BtsCatalogExplorer
$Catalog.ConnectionString = "SERVER = #server_address; DATABASE=BizTalkMgmtDB; Integrated Security=SSPI"
#Implementing functions
CreateRPandRL
CreateSendPorts
Start-Sleep -Seconds 60
<#
#===BizTalk Application===#
$app = $Catalog.AddNewApplication()
$app.Name = "TestingPowershellScript"
$app.CreateRPandRL()
$app.CreateSendPorts()
#>
Hoo boy, this takes me back a few years, I'm glad I'm not the only one to struggle with this. You want to leave that alone and switch to the BizTalk PowerShell Extensions (information on this is sketchy), they are sooooooo much easier to work with in PowerShell.
I cobbled this together from some scripts I used, and left out some of the fancy stuff, but what you want is basically:
$InitializeDefaultBTSDrive = $false
Import-Module "$env:BTSINSTALLPATH\SDK\Utilities\PowerShell\BizTalkFactory.PowerShell.Extensions.dll" -WarningAction Ignore
New-PSDrive -Name BizTalk -PSProvider BizTalk -Root BizTalk:\ -Instance $DatabaseName -Database $BizTalkMgmtDb
This opens up a whole world of goodies, because it's loaded as a PSDrive, you can navigate round it, create things, delete things, use it all as native as any other drive/filesystem, such as:
Get-ChildItem "BizTalk:\All Artifacts\Receive Locations"
Get-ChildItem "BizTalk:\All Artifacts\Receive Locations" | Disable-ReceiveLocation
Get-ChildItem "BizTalk:\Platform Settings\Host Instances" | Stop-HostInstance
Get-ChildItem "BizTalk:\Platform Settings\Host Instances" | Where-Object { $_.IsDisabled -eq $false } | Start-HostInstance
Get-ChildItem "BizTalk:\All Artifacts\Receive Locations" | Enable-ReceiveLocation
Get-ChildItem -Path "BizTalk:\Health and Activity\Service Instances"
There's so much more than the above, and none of this is what you really asked for, what you actually want is:
Import-Bindings -Path "BizTalk:" -Source $bindings
Where $bindings is your XML bindings file.
My advice, don't even try this. Most of the useful settings for Adapters are not exposed by any API so this will get you maybe half way at most.
Instead, script the import of a binding file which does support all settings for all Adapters.

net files /close does not work as expected

We have many self-written applications which are executed in a network location. Whenever a developer wants to publish a new version of this application, I have to close all opened files on the server where the app is located, by going to:
computer management > Shared Folders > Opened Files
then choose all the files of the application - right click and close.
I want the developers to do that by themselves via PowerShell, so I wrote a Unlock-Files function. This part of the function should close the files, with the net files $id /close call:
$result = Invoke-Command -ComputerName $server -Credential $cred -ArgumentList $workpath, $server {
param($workpath,$server)
$list = New-Object System.Collections.ArrayList
$ErrorActionPreference = "SilentlyContinue"
$adsi = [adsi]"WinNT://./LanmanServer"
$resources = $adsi.psbase.Invoke("resources") | % {
[PSCustomObject] #{
ID = $_.gettype().invokeMember("Name","GetProperty",$null,$_,$null)
Path = $_.gettype().invokeMember("Path","GetProperty",$null,$_,$null)
OpenedBy = $_.gettype().invokeMember("User","GetProperty",$null,$_,$null)
LockCount = $_.gettype().invokeMember("LockCount","GetProperty",$null,$_,$null)
}
}
$resources | ? { $_.Path -like $workpath } | tee -Variable Count | % {
$id = $_.ID
net files $id /close > $null
if ($LASTEXITCODE -eq 0) { $list.add("File successfully unlocked $($_.path)") > $null }
else { $list.add("File not unlocked (maybe still open somewhere): $($_.path)") > $null }
}
if (!( ($count.count) -ge 1 )) { $list.add("No Files to unlock found in $workpath on $server") > $null }
$list
}
I expect net files /close to behave the same way as the manual way I do directly on the server described above, but it doesn't behave like this at all. It sometimes closes the file, sometimes not. But it never closes all the necessary files, so the developer can not publish his application. also, net files almost never finishes with LastExitCode 0, but I can't get my head around why.
Is there a way to force net files to really close all the files?
why would net files behave different than closing the files manually?
Is there a native PowerShell way to do this?
Is there a list of last exit codes, so I can wrap my head around this issue further?
Thank you!

Automate Removal of a Path System Environment Variable

I'm running into an issue where a removal and reinstallation of a particular application is creating duplicate entries in System Variables under Path. So for example, it's showing C:\Apps\folder;C:\Apps\folder;typical entries.
While this is not causing a problem with functionality of the app, I'd prefer to not have the entry in there twice (or more if it requires an additional removal/installation). I want to automate something so I don't have to go into each system and manually remove one of those entries.
Can this be done through either a batch file or a PowerShell script? I'm not able to find a way, but hopefully someone here will know a way. It's fine if the method removes both entries, as I can add something to the script to add one of them back. One important note, I need to make sure everything else under Path is left intact.
Here is a script (taken from a Microsoft repository) that I used to do the exact same thing.
$RegKey = ([Microsoft.Win32.Registry]::LocalMachine).OpenSubKey("SYSTEM\CurrentControlSet\Control\Session Manager\Environment", $True)
$PathValue = $RegKey.GetValue("Path", $Null, "DoNotExpandEnvironmentNames")
Write-host "Original path :" + $PathValue
$PathValues = $PathValue.Split(";", [System.StringSplitOptions]::RemoveEmptyEntries)
$IsDuplicate = $False
$NewValues = #()
ForEach ($Value in $PathValues)
{
if ($NewValues -notcontains $Value)
{
$NewValues += $Value
}
else
{
$IsDuplicate = $True
}
}
if ($IsDuplicate)
{
$NewValue = $NewValues -join ";"
$RegKey.SetValue("Path", $NewValue, [Microsoft.Win32.RegistryValueKind]::ExpandString)
Write-Host "Duplicate PATH entry found and new PATH built removing all duplicates. New Path :" + $NewValue
}
else
{
Write-Host "No Duplicate PATH entries found. The PATH will remain the same."
}
$RegKey.Close()

TFS 2015 no longer adds build number to Global List upon build complete?

In TFS 2015 new build system, did the functionality to automatically add build number to Global List (Build - Project Name) upon build complete removed?
Do I need to write a custom PowerShell task to accomplish this?
Note: XAML builds still add build number to Global List as it did before.
Since many features are still missing in the vNext build system, I've made a PowerShell script that do the Job.
In a near futur, I plan to update this script to support IntegratedIn field filling and to convert the script as a custom build task.
[CmdletBinding(SupportsShouldProcess=$false)]
param()
function Update-GlobalListXml
{
[CmdletBinding(SupportsShouldProcess=$false)]
param(
[xml]$globalListsDoc,
[parameter(Mandatory=$true)][string][ValidateNotNullOrEmpty()]$glName,
[parameter(Mandatory=$true)][string][ValidateNotNullOrEmpty()]$buildNumber
)
Write-Verbose "Checking whether '$glName' exists"
$buildList = $globalListsDoc.GLOBALLISTS.GLOBALLIST | Where-Object { $_.name -eq $glName }
if ($buildList -eq $null)
{
Write-Host "GlobalList '$glName' does not exist and will be created"
$globalLists = $globalListsDoc.GLOBALLISTS
if($globalLists.OuterXml -eq $null)
{
$newDoc = [xml]"<gl:GLOBALLISTS xmlns:gl="""http://schemas.microsoft.com/VisualStudio/2005/workitemtracking/globallists"""></gl:GLOBALLISTS>"
$globalLists = $newDoc.GLOBALLISTS
}
$globalList = $globalLists.OwnerDocument.CreateElement("GLOBALLIST")
$globalList.SetAttribute("name", $glName)
$buildList = $globalLists.AppendChild($globalList)
}
if(($buildList.LISTITEM | where-object { $_.value -eq $buildNumber }) -ne $null)
{
throw "The LISTITEM value: '$buildNumber' already exists in the GLOBALLIST: '$glName'"
}
Write-Host "Adding '$buildNumber' as a new LISTITEM in '$glName'"
$build = $buildList.OwnerDocument.CreateElement("LISTITEM")
$build.SetAttribute("value", $buildNumber)
$buildList.AppendChild($build) | out-null
return $buildList.OwnerDocument
}
function Invoke-GlobalListAPI()
{
[CmdletBinding(SupportsShouldProcess=$false)]
param(
[parameter(Mandatory=$true)][Microsoft.TeamFoundation.WorkItemTracking.Client.WorkItemStore]$wiStore,
[parameter(Mandatory=$true,ParameterSetName="Import")][switch]$import,
[parameter(Mandatory=$true,ParameterSetName="Import")][xml]$globalLists,
[parameter(ParameterSetName="Export")][switch]$export
)
try {
if($import)
{
$wiStore.ImportGlobalLists($globalLists.OuterXml) # Account must be explicitly in the Project Administrator Group
}
if($export)
{
return [xml]$wiStore.ExportGlobalLists()
}
}
catch [Microsoft.TeamFoundation.TeamFoundationServerException] {
Write-Error "An error has occured while exporting or importing GlobalList"
throw $_
}
}
function Get-WorkItemStore()
{
[CmdletBinding(SupportsShouldProcess=$false)]
param(
[parameter(Mandatory=$true)][string][ValidateNotNullOrEmpty()]$tpcUri,
[parameter(Mandatory=$true)][string][ValidateNotNullOrEmpty()]$agentWorker
)
# Loads client API binaries from agent folder
$clientDll = Join-Path $agentWorker "Microsoft.TeamFoundation.Client.dll"
$wiTDll = Join-Path $agentWorker "Microsoft.TeamFoundation.WorkItemTracking.Client.dll"
[System.Reflection.Assembly]::LoadFrom($clientDll) | Write-Verbose
[System.Reflection.Assembly]::LoadFrom($wiTDll) | Write-Verbose
try {
Write-Host "Connecting to $tpcUri"
$tfsTpc = [Microsoft.TeamFoundation.Client.TfsTeamProjectCollectionFactory]::GetTeamProjectCollection($tpcUri)
return $tfsTpc.GetService([Microsoft.TeamFoundation.WorkItemTracking.Client.WorkItemStore])
}
catch [Microsoft.TeamFoundation.TeamFoundationServerException] {
Write-Error "An error has occured while retrieving WorkItemStore"
throw $_
}
}
function Get-WITDataStore64
{
[CmdletBinding(SupportsShouldProcess=$false)]
param()
if($env:VS140COMNTOOLS -eq $null)
{
throw New-Object System.InvalidOperationException "Visual Studio 2015 must be installed on the build agent" # TODO: Change it by checking agent capabilities
}
$idePath = Join-Path (Split-Path -Parent $env:VS140COMNTOOLS) "IDE"
return Get-ChildItem -Recurse -Path $idePath -Filter "Microsoft.WITDataStore64.dll" | Select-Object -First 1 -ExpandProperty FullName
}
function Update-GlobalList
{
[CmdletBinding(SupportsShouldProcess=$false)]
param()
# Get environment variables
$tpcUri = $env:SYSTEM_TEAMFOUNDATIONCOLLECTIONURI
Write-Verbose "Team Project Collection Url: '$tpcUri'"
$teamProjectName = $env:SYSTEM_TEAMPROJECT
Write-Verbose "Team Project: '$teamProjectName'"
$buildNumber = $env:BUILD_BUILDNUMBER
Write-Verbose "Build Number: '$buildNumber'"
$agentHome = $env:AGENT_HOMEDIRECTORY
Write-Verbose "Agent home direrctory: '$agentHome'"
$globalListName = "Builds - $teamProjectName"
Write-Verbose "GlobalList name: '$teamProjectName'"
# Copy 'Microsoft.WITDataStore64.dll' from Visual Studio directory to AgentBin directory if it does not exist
$agentWorker = Join-Path $agentHome "agent\Worker"
$targetPath = Join-Path $agentWorker "Microsoft.WITDataStore64.dll" # Only compatible with x64 process #TODO use constant instead
if(-not (Test-Path $targetPath))
{
$wITDataStore64FilePath = Get-WITDataStore64
Write-Host "Copying $wITDataStore64FilePath to $targetPath"
Copy-Item $wITDataStore64FilePath $targetPath | Write-Verbose
}
$wiStore = Get-WorkItemStore -tpcUri $tpcUri -agentWorker $agentWorker
# Retrive GLOBALLISTS
$xmlDoc = Invoke-GlobalListAPI -export -wiStore $wiStore
$gls2 = Update-GlobalListXml -globalListsDoc $xmlDoc -glName $globalListName -buildNumber $buildNumber
Invoke-GlobalListAPI -import -globalLists $gls2 -wiStore $wiStore
}
Update-GlobalList
Here is the link of the Github repo, feedbacks are welcome => https://github.com/GregoryOtt/UpdateWiBuildNum/blob/master/Update-GlobalList.ps1
[disclaimer - I work on the new build system]
That global list on the workitem is a mechanism that dated back to the original release of TFS. It's one that sort of worked in that day and age (days of nightly builds, pre-CI and CD agility). It's starts to fall apart and doesn't show as proper relationships in TFS. I worked on WIT at that time and we needed a queryable mechanism and that's what we had (blame me :)
So, when we started a new build system, we didn't want to rebuild things and repeat the same mistakes. We're trying to take an agile, incremental approach to a better build system.
In the next sprint (88), we are starting work on proper links between builds and workitems and the WIT team is also doing work to make them more first class. The first thing you'll see is a link on the WIT form and that should hopefully make QU1 as well (at least parts of it).
We realize this does leave a few gaps but we are working to close them (gated and label sources being two others) and hopefully in a better way for a better long term product.
As far as a workaround goes, it should be possible to automate via powershell and our clients but we don't have anything canned for others to use.

How to pin to start menu using PowerShell

I can pin some programs to taskbar on Win7 using PowerShell.
$shell = new-object -com "Shell.Application"
$folder = $shell.Namespace('C:\Windows')
$item = $folder.Parsename('notepad.exe')
$verb = $item.Verbs() | ? {$_.Name -eq 'Pin to Tas&kbar'}
if ($verb) {$verb.DoIt()}
How do I modify the above code to pin a program to the Start menu?
Another way
$sa = new-object -c shell.application
$pn = $sa.namespace($env:windir).parsename('notepad.exe')
$pn.invokeverb('startpin')
Or unpin
$pn.invokeverb('startunpin')
Use the code below
$shell = new-object -com "Shell.Application"
$folder = $shell.Namespace('C:\Windows')
$item = $folder.Parsename('notepad.exe')
$verb = $item.Verbs() | ? {$_.Name -eq 'Pin to Start Men&u'}
if ($verb) {$verb.DoIt()}
Note: the change is in the fourth line.
The main problem with most of the solution is that they enumerate the verbs on a file, search for the string to perform the action (“Pin to Startmenu” etc.) and then execute it. This does not work if you need to support 30+ languages in your company, except you use external function to search for the localized command (see answer from shtako-verflow).
The answer from Steven Penny is the first that is language neutral and does not need any external code. It uses the verbs stored in the registry HKEY_CLASSES_ROOT\CLSID\{90AA3A4E-1CBA-4233-B8BB-535773D48449} and HKEY_CLASSES_ROOT\CLSID\{a2a9545d-a0c2-42b4-9708-a0b2badd77c8}
Based on this, here’s the code we are now using:
function PinToTaskbar {
param([Parameter(Mandatory=$true)][string]$FilePath)
ExecuteVerb $FilePath "taskbarpin"
}
function UnpinFromTaskbar {
param([Parameter(Mandatory=$true)][string]$FilePath)
ExecuteVerb $FilePath "taskbarunpin"
}
function PinToStartmenu {
param([Parameter(Mandatory=$true)][string]$FilePath)
ExecuteVerb $FilePath "startpin"
}
function UnpinFromStartmenu {
param([Parameter(Mandatory=$true)][string]$FilePath)
ExecuteVerb $FilePath "startunpin"
}
function ExecuteVerb {
param(
[Parameter(Mandatory=$true)][string]$File,
[Parameter(Mandatory=$true)][string]$Verb
)
$path = [System.Environment]::ExpandEnvironmentVariables($File)
$basePath = split-path $path -parent #retrieve only the path File=C:\Windows\notepad.exe -> C:\Windows
$targetFile = split-path $path -leaf #retrieve only the file File=C:\Windows\notepad.exe -> notepad.exe
$shell = new-object -com "Shell.Application"
$folder = $shell.Namespace($basePath)
if ($folder)
{
$item = $folder.Parsename($targetFile)
if ($item)
{
$item.invokeverb($Verb)
# "This method does not return a value." (http://msdn.microsoft.com/en-us/library/windows/desktop/bb787816%28v=vs.85%29.aspx)
# Therefore we have no chance to know if this was successful...
write-host "Method [$Verb] executed for [$path]"
}
else
{
write-host "Target file [$targetFile] not found, aborting"
}
}
else
{
write-host "Folder [$basePath] not found, aborting"
}
}
#PinToTaskbar "%WINDIR%\notepad.exe"
#UnpinFromTaskbar "%WINDIR%\notepad.exe"
PinToStartmenu "%WINDIR%\notepad.exe"
#UnpinFromStartmenu "%WINDIR%\notepad.exe"
See the script (international) here : http://gallery.technet.microsoft.com/scriptcenter/b66434f1-4b3f-4a94-8dc3-e406eb30b750
If you want to add an action like Pin to Modern UI interface (Windows 8), at $verbs, add 51201
Steven Penny's second answer above worked well for me. Here are a couple more tidbits.
It's doing COM through PowerShell, so you can do the same thing with pretty much any COM client. For example, here's an AutoHotkey version.
Shell := ComObjCreate("Shell.Application")
Target := Shell.Namespace(EnvGet("WinDir")).ParseName("Notepad.exe")
Target.InvokeVerb("startpin")
VBScript or InnoSetup would look almost the same except for the function used to create the object.
I also found that I have one program that pinned OK, but didn't have the right icon and/or description because of limitations in the compiler. I just made a little 1-line WinForms app that starts the target with Process.Start, and then added the appropriate icon, and the name I wanted in the Start Menu in the Title property in AppInfo.cs.