Skip to content

Instantly share code, notes, and snippets.

@rleap-m
Last active March 4, 2025 20:19
Show Gist options
  • Save rleap-m/253257fbe051a67151e72aa8b4e45333 to your computer and use it in GitHub Desktop.
Save rleap-m/253257fbe051a67151e72aa8b4e45333 to your computer and use it in GitHub Desktop.
PowerShell wrapper which runs containerized TestRail Manager
function trman {
<#
.SYNOPSIS
Runs (containerized) TestRail Manager CLI using credentials defined by env vars
.DESCRIPTION
This tool interacts with both the TestRail Server and the Jenkins Server to faciliate quickly creating
test runs (TestRail) which are categorized under a milestone and job templates (Jenkins) which with
only some minor edits in the UI can then be executed. The tool accepts credentials via environment variables
for TestRail (TESTRAIL_USER, TESTRAIL_PASSWORD) and Jenkins (JENKINS_USER, JENKINS_PASSWORD).
.NOTES
Named non-mandatory params don't work as expected with ValueFromRemainingArguments so avoiding named parms
.EXAMPLE
trman --version
Displays the version of the TestRail Manager command line tool
.EXAMPLE
trman tool tab-to-yaml --file-name ./patch_2025-01B/permutations.tsv `
--parent-milestone-id '472' `
--set-a 'rhel/7.9,rhel/8.10' `
--set-b 'rhel/9.4,windows/2022' `
--set-c 'ubuntu/20.04' `
--output-file ./patch_2025-01B/milestone.yaml
Given a proper permutations file (from ver X of MCR/MKE/MSR to ver Y of MCR/MKE/MSR), this command produces a (child)
milestone (round) YAML file. Certain test runs (MCR+MKE+MSR) only work with specific OS platforms (cgroup v2 support
needed for ex) and therefore this command accommodates "platform sets" (--set-a, b, c) allowing the user to define
here which platforms are associated with which product test runs.
.EXAMPLE
trman milestone create-from-yaml --file ./patch_2024-11B/milestone.yaml
Creates a child milestone (test round) and test plans on the TestRail server based on the YAML file provided
.EXAMPLE
trman milestone yaml 484 | Out-File -FilePath ./patch_2024-11B/milestone_484.yaml
Outputs YAML representing the milestone (allowing you to edit it locally) as it exists on the TestRail server
.EXAMPLE
trman milestone update --file ./patch_2024-11B/milestone_484.yaml
Takes the (presumably edited) milestone YAML file and updates the changes accordingly on the TestRail server
.EXAMPLE
trman milestone show 484
Displays a tree diagram of the milestone (given a valid milestone ID) provided, including rounds and test runs
.EXAMPLE
trman tool create-template-job 161459
Creates a job in Jenkins w/out the platforms populated so you can quickly review it with the Web UI, make edits, and execute
To obtain a job id to supply the above command use the 'trman milestone show' command and select the appropriate run:
...
├- plan.id: 161459
| .name: 01: Upgrade Patch Ver. default 2.9 23.0.16/3.7.17/2.9.22 → 23.0.16/3.7.18-rc1/2.9.22
...
.EXAMPLE
trman milestone clone 484
Clones the round (child milestone). This is useful when updated container product builds have been released
(e.g. tp1 -> tp2). When this occurs (new product builds released) you will need to:
1. Conclude any runs in the existing test round
2. Clone the round (child milestone)
3. Download the cloned milestone (as YAML)
4. Edit it with the new product set (also update the name)
5. Sync it back to TestRail
6. Create the template job(s) listed in the milestone in Jenkins and begin executing the jobs when ready
.EXAMPLE
trman report milestone 486 | Tee-Object -Path ./patch_2025-01/report-milestone-486.md
Creates a markdown report of the milestone (given a valid milestone ID) provided, including rounds and test runs. The contents
of this markdown file can then be copy/pasted directly into the Confluence report page for the milestone.
.LINK
https://mirantis.jira.com/wiki/x/A4CwWQE
#>
[cmdletbinding()]
param (
[Parameter(ValueFromRemainingArguments = $true)]
[string[]] $Args
)
for ($i = 0; $i -lt $Args.Count; $i++) {
Write-Debug "${i}: $($Args[$i])"
}
if ($env:TESTRAIL_MANAGER_IMAGE) {
[string] $TestRailMgrImage = $env:TESTRAIL_MANAGER_IMAGE
}
else {
[string] $TestRailMgrImage = 'msr.ci.mirantis.com/ekitagawa/testrailmanager:0.0.7'
}
Write-Debug "Using TestRail container image [$TestRailMgrImage]"
if ($Args.Contains('--help')) {
Write-Verbose "Running docker container..."
if ($DebugPreference -ne 'SilentlyContinue') {
Write-Verbose "Running container with debug env flag"
docker container run --rm -e TESTRAIL_LOG='DEBUG' $TestRailMgrImage --help
}
else {
docker container run --rm $TestRailMgrImage --help
}
Write-Verbose "Running docker container complete."
}
else {
if (-not($env:TESTRAIL_USER)) {
Write-Warning "TestRail user env var (TESTRAIL_USER) not defined."
}
if (-not($env:TESTRAIL_PASSWORD)) {
Write-Warning "TestRail password env var (TESTRAIL_PASSWORD) not defined."
}
if (($Args[0] -eq 'tool') -and ($Args[1] -eq 'create-template-job')) {
if (-not($env:JENKINS_USER)) {
Write-Warning "Jenkins user env var (JENKINS_USER) not defined."
}
if (-not($env:JENKINS_PASSWORD)) {
Write-Warning "Jenkins password env var (JENKINS_PASSWORD) not defined."
}
}
if (($Args[0] -eq 'tool') -and ($Args[1] -eq 'tab-to-yaml')) {
if ($Args.Contains('--file-name')) {
[int] $permutationsIndex = $Args.IndexOf('--file-name') + 1
[string] $permutationsPath = $Args[$permutationsIndex]
$Args[$permutationsIndex] = "/data/permutations.tsv" # modify the path to that of the file in the container
if (Test-Path -Path $permutationsPath -PathType Leaf) {
if ($Args.Contains('--output-file')) {
[int] $outputFileIndex = $Args.IndexOf('--output-file') + 1
[string] $outputFileHostPath = $Args[$outputFileIndex]
[string] $milestoneFile = Split-Path -Path $outputFileHostPath -Leaf
$Args[$outputFileIndex] = $milestoneFile # trman CLI expects a filename (not a path to a file)
if ($milestoneFile) {
Write-Verbose "Running docker container with a volume to access permuations file [$permutationsPath]."
Write-Debug 'docker container run -d `'
Write-Debug " -v `"$($permutationsPath):$($Args[$permutationsIndex])`" ``"
if ($DebugPreference -ne 'SilentlyContinue') {
Write-Debug " -e TESTRAIL_LOG='DEBUG'"
}
Write-Debug " -e TESTRAIL_USER=`$env:TESTRAIL_USER ``"
Write-Debug " -e TESTRAIL_PASSWORD=`$env:TESTRAIL_PASSWORD ``"
Write-Debug " -e JENKINS_USER=`$env:JENKINS_USER ``"
Write-Debug " -e JENKINS_PASSWORD=`$env:JENKINS_PASSWORD ``"
Write-Debug " $TestRailMgrImage $Args"
if ($DebugPreference -ne 'SilentlyContinue') {
$containerId = docker container run -d `
-v "$($permutationsPath):$($Args[$permutationsIndex])" `
-e TESTRAIL_LOG='DEBUG' `
-e TESTRAIL_USER=$env:TESTRAIL_USER `
-e TESTRAIL_PASSWORD=$env:TESTRAIL_PASSWORD `
-e JENKINS_USER=$env:JENKINS_USER `
-e JENKINS_PASSWORD=$env:JENKINS_PASSWORD `
$TestRailMgrImage $Args
}
else {
$containerId = docker container run -d `
-v "$($permutationsPath):$($Args[$permutationsIndex])" `
-e TESTRAIL_USER=$env:TESTRAIL_USER `
-e TESTRAIL_PASSWORD=$env:TESTRAIL_PASSWORD `
-e JENKINS_USER=$env:JENKINS_USER `
-e JENKINS_PASSWORD=$env:JENKINS_PASSWORD `
$TestRailMgrImage $Args
}
Write-Verbose "Running docker container complete."
docker wait $containerId | Out-Null
docker container cp "$($containerId):/app/$milestoneFile" $outputFileHostPath
if (-not(Test-Path -Path $outputFileHostPath -PathType Leaf)) {
Write-Warning "Milestone file [$outputFileHostPath] not generated."
}
else {
Write-Verbose "Removing container with ID [$containerId]."
docker container rm $containerId | Out-Null
Write-Verbose "Removing container with ID [$containerId] complete."
}
}
else {
Write-Warning "Milestone output file not provided!"
}
}
else {
Write-Warning "Option '--output-file' not provided."
}
}
else {
Write-Warning "Permutations file [$permutationsPath] missing!"
}
}
else {
Write-Warning "Option '--file-name' not provided."
}
}
elseif (($Args[0] -eq 'milestone') -and (($Args[1] -eq 'create-from-yaml') -or ($Args[1] -eq 'update'))) {
if ($Args.Contains('--file')) {
[int] $milestoneIndex = $Args.IndexOf('--file') + 1
[string] $milestonePath = $Args[$milestoneIndex]
if (Test-Path -Path $milestonePath -PathType Leaf) {
Write-Verbose "Running docker container with a volume to access to milestone file [$milestonePath]."
$Args[$milestoneIndex] = Join-Path '/data' -ChildPath (Split-Path -Path $milestonePath -Leaf) # modify the path to that of the file in the container
Write-Debug 'docker container run --rm `'
Write-Debug " -v `"$($milestonePath):$($Args[$milestoneIndex])`" ``"
if ($DebugPreference -ne 'SilentlyContinue') {
Write-Debug " -e TESTRAIL_LOG='DEBUG'"
}
Write-Debug " -e TESTRAIL_USER=`$env:TESTRAIL_USER ``"
Write-Debug " -e TESTRAIL_PASSWORD=`$env:TESTRAIL_PASSWORD ``"
Write-Debug " -e JENKINS_USER=`$env:JENKINS_USER ``"
Write-Debug " -e JENKINS_PASSWORD=`$env:JENKINS_PASSWORD ``"
Write-Debug " $TestRailMgrImage $Args"
if ($DebugPreference -ne 'SilentlyContinue') {
docker container run --rm `
-v "$($milestonePath):$($Args[$milestoneIndex])" `
-e TESTRAIL_LOG='DEBUG' `
-e TESTRAIL_USER=$env:TESTRAIL_USER `
-e TESTRAIL_PASSWORD=$env:TESTRAIL_PASSWORD `
-e JENKINS_USER=$env:JENKINS_USER `
-e JENKINS_PASSWORD=$env:JENKINS_PASSWORD `
$TestRailMgrImage $Args
}
else {
docker container run --rm `
-v "$($milestonePath):$($Args[$milestoneIndex])" `
-e TESTRAIL_USER=$env:TESTRAIL_USER `
-e TESTRAIL_PASSWORD=$env:TESTRAIL_PASSWORD `
-e JENKINS_USER=$env:JENKINS_USER `
-e JENKINS_PASSWORD=$env:JENKINS_PASSWORD `
$TestRailMgrImage $Args
}
Write-Verbose "Running docker container complete."
}
else {
Write-Warning "Milestone file [$milestonePath] does not exist!"
}
}
else {
Write-Warning "Option '--file' not provided."
}
}
else {
Write-Verbose "Running docker container..."
if ($DebugPreference -ne 'SilentlyContinue') {
docker container run --rm `
-e TESTRAIL_LOG='DEBUG' `
-e TESTRAIL_USER=$env:TESTRAIL_USER `
-e TESTRAIL_PASSWORD=$env:TESTRAIL_PASSWORD `
-e JENKINS_USER=$env:JENKINS_USER `
-e JENKINS_PASSWORD=$env:JENKINS_PASSWORD `
$TestRailMgrImage $Args
}
else {
docker container run --rm `
-e TESTRAIL_USER=$env:TESTRAIL_USER `
-e TESTRAIL_PASSWORD=$env:TESTRAIL_PASSWORD `
-e JENKINS_USER=$env:JENKINS_USER `
-e JENKINS_PASSWORD=$env:JENKINS_PASSWORD `
$TestRailMgrImage $Args
}
Write-Verbose "Running docker container complete."
}
}
}
function Get-TestRailTestRun {
<#
.SYNOPSIS
Gets the TestRail test run(s) belonging to a milestone
.PARAMETER MilestoneId
ID of a milestone
.PARAMETER TestRunId
ID of a Test Run (plan.id) within the milestone
.EXAMPLE
Get-TestRailTestRun -MilestoneId 469
Gets all the test runs within the milestone
.EXAMPLE
Get-TestRailTestRun -MilestoneId 469 -TestRunId 161002
Gets the specified test run belonging to the milestone
.EXAMPLE
Get-TestRailTestRun -MilestoneId 469 | Where-Object {(!$_.crossv) -and (!$_.cncf)} |
Select-Object -Property * -ExcludeProperty name,os,crossv,cncf | Format-Table -Wrap
Gets the test runs which are not cross version upgrades or CNCF Conformance tests
.EXAMPLE
$propBag = @('id','shortname','swarmo','crossv','MCRVerBase','MCRVerUp','MKEVerBase','MKEVerUp','MSRVerBase','MSRVerUp')
Get-TestRailTestRun -MilestoneId 469 | Select-Object -Property $propBag | Format-Table
Gets the 'upgrade from' and 'upgrade to' properties of all the Mirantis products for each test run
#>
[CmdletBinding()]
param (
[Parameter(Mandatory = $true)]
[int16] $MilestoneId,
[Parameter(Mandatory = $false)]
[int[]] $TestRunId
)
$installed = Try { $null = Get-command -Name yj -ErrorAction Stop; $true } catch { $false }
if (-not($installed)) {
Write-Warning "Required utility [yj] not installed. See https://github.com/sclevine/yj for installation details."
return
}
$milestone = trman milestone yaml $MilestoneId | yj | ConvertFrom-Json
if ($milestone) {
if ($TestRunId) {
$milestone.test_plans = $milestone.test_plans | Where-Object { $_.id -in $TestRunId }
if ($null -eq $milestone.test_plans) {
Write-Warning "Unable to obtain a test run with ID(s) [$($TestRunId -join ',')] for milestone ID [$MilestoneId]"
}
}
foreach ($testPlan in $milestone.test_plans) {
Clear-Variable -Name 'mode','sut','type','script','osList','sutFrom','sutTo','fromMcr','fromMke','fromMsr','toMcr','toMke','toMsr' -ErrorAction SilentlyContinue
Clear-Variable -Name 'fromMcrVer','fromMcrChannel','toMcrVer','toMcrChannel','fromMkeVer','fromMkeRepo','toMkeVer','toMkeRepo' -ErrorAction SilentlyContinue
$mode,$sut,$type,$script,$osList = $testPlan.description -split [System.Environment]::NewLine
$sutFrom,$sutTo = $sut.Substring('SUT:'.Length).Trim() -split ' '
$fromMcr,$fromMke,$fromMsr = $sutFrom -split ','
$toMcr,$toMke,$toMsr = $sutTo -split ','
if ($fromMcr) {
if ($fromMcr -match '\d') {
$fromMcrVer = $fromMcr.Substring($fromMcr.IndexOf($Matches[0]))
$fromMcrChannel = $fromMcr.Substring(0,$fromMcr.IndexOf($Matches[0])-1)
}
}
if ($toMcr) {
if ($toMcr -match '\d') {
$toMcrVer = $toMcr.Substring($toMcr.IndexOf($Matches[0]))
$toMcrChannel = $toMcr.Substring(0,$toMcr.IndexOf($Matches[0])-1)
}
}
if ($fromMke) {
$fromMkeVer = $fromMke.Substring($fromMke.LastIndexOf('/')+1)
$fromMkeRepo = $fromMke.Substring(0,$fromMke.LastIndexOf('/'))
}
if ($toMke) {
$toMkeVer = $toMke.Substring($toMke.LastIndexOf('/')+1)
$toMkeRepo = $toMke.Substring(0,$toMke.LastIndexOf('/'))
}
if ($fromMsr) {
$fromMsrVer = $fromMsr.Substring($fromMsr.LastIndexOf('/')+1)
$fromMsrRepo = $fromMsr.Substring(0,$fromMsr.LastIndexOf('/'))
}
if ($toMsr) {
$toMsrVer = $toMsr.Substring($toMsr.LastIndexOf('/')+1)
$toMsrRepo = $toMsr.Substring(0,$toMsr.LastIndexOf('/'))
}
[PSCustomObject]@{
parentid = $MilestoneId
id = $testPlan.id
name = $testPlan.Name
shortname = $testPlan.Name.Substring(0,15).trim() + '...'
mode = $mode.Substring('Mode:'.Length).Trim()
swarmo = $testPlan.Name.Contains('swarm-only')
crossv = $testPlan.Name.Contains('Cross')
cncf = $testPlan.Name.Contains('CNCF')
testcase = $script.Substring('SCRIPT:'.Length).Trim()
MCRVerBase = $fromMcrVer
MCRChanBase = $fromMcrChannel
MCRVerUp = $toMcrVer
MCRChanUp = $toMcrChannel
MKEVerBase = $fromMkeVer
MKERepoBase = $fromMkeRepo
MKEVerUp = $toMkeVer
MKERepoUp = $toMkeRepo
MSRVerBase = $fromMsrVer
MSRRepoBase = $fromMsrRepo
MSRVerUp = $toMsrVer
MSRRepoUp = $toMsrRepo
OS = @($osList.Substring('OS:'.Length).Trim() -split ',')
}
}
}
else {
Write-Warning "Unable to obtain milestone data for milestone ID [$MilestoneId]"
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment