Octopus.AwsRunScript exported 2023-11-17 by mcasperson belongs to ‘Octopus’ category.
This step queries each workspace in the Terraform state for downstream Octopus CaC enabled projects, extracts the Git repo associated with the CaC project, and merges any changes so long as there are no merge conflicts.
If there is a merge conflict between the upstream and downstream repos, instructions for manually resolving the conflict are provided.
Parameters
When steps based on the template are included in a project’s deployment process, the parameters below can be set.
Octopus Spaces
FindConflicts.Octopus.Spaces =
An optional newline-separated list of space names with projects to merge changes into. Leave this field blank to merge changes to projects in all spaces.
Octopus Projects
FindConflicts.Octopus.Projects =
A newline-separated list of projects to merge changes into. Leave this field blank to merge changes to all projects.
Git Username
FindConflicts.Git.Credentials.Username = x-access-token
The git repo username. When using GitHub with an access token, the value is x-access-token
.
Git Password
FindConflicts.Git.Credentials.Password =
The git repo password or access token.
Git Protocol
FindConflicts.Git.Url.Protocol = https
The git repo protocol.
Git Hostname
FindConflicts.Git.Url.Host = github.com
The git repo host name.
Git Organization
FindConflicts.Git.Url.Organization =
The git repo owner or organization i.e. owner
in the url https://github.com/owner/repo
.
Git Template Repo
FindConflicts.Git.Url.Template =
The repo holding the upstream, or template, CaC project i.e. repo
in the url https://github.com/owner/repo
.
AWS Region
FindConflicts.Terraform.Backend.S3Region =
The AWS region hosting the S3 bucket persisting the Terraform state.
S3 Key
FindConflicts.Terraform.Backend.S3Key = Project_#{Octopus.Project.Name | Replace "[^A-Za-z0-9]" "_"}
The name of the file in the S3 bucket hosting the Terraform state.
S3 Bucket
FindConflicts.Terraform.Backend.S3Bucket =
The name of the S3 bucket hosting the Terraform state.
AWS Account
FindConflicts.Terraform.Aws.Account =
The AWS account used to access the S3 bucket.
Script body
Steps based on this template will execute the following PowerShell script.
# Check to see if $IsWindows is available
if ($null -eq $IsWindows)
{
Write-Host "Determining Operating System..."
$IsWindows = ([System.Environment]::OSVersion.Platform -eq "Win32NT")
$IsLinux = ([System.Environment]::OSVersion.Platform -eq "Unix")
}
Function Get-GitExecutable
{
# Define parameters
param (
$WorkingDirectory
)
# Define variables
$gitExe = "PortableGit-2.41.0.3-64-bit.7z.exe"
$gitDownloadUrl = "https://github.com/git-for-windows/git/releases/download/v2.41.0.windows.3/$gitExe"
$gitDownloadArguments = @{ }
$gitDownloadArguments.Add("Uri", $gitDownloadUrl)
$gitDownloadArguments.Add("OutFile", "$WorkingDirectory/git/$gitExe")
# This makes downloading faster
$ProgressPreference = 'SilentlyContinue'
# Check to see if git subfolder exists
if ((Test-Path -Path "$WorkingDirectory/git") -eq $false)
{
# Create subfolder
New-Item -Path "$WorkingDirectory/git" -ItemType Directory | Out-Null
}
# Check PowerShell version
if ($PSVersionTable.PSVersion.Major -lt 6)
{
# Use basic parsing is required
$gitDownloadArguments.Add("UseBasicParsing", $true)
}
# Download Git
Write-Host "Downloading Git ..."
Invoke-WebRequest @gitDownloadArguments
# Extract Git
$gitExtractArguments = @()
$gitExtractArguments += "-o"
$gitExtractArguments += "$WorkingDirectory\git"
$gitExtractArguments += "-y"
$gitExtractArguments += "-bd"
Write-Host "Extracting Git download ..."
& "$WorkingDirectory\git\$gitExe" $gitExtractArguments
# Wait until unzip action is complete
while ($null -ne (Get-Process | Where-Object { $_.ProcessName -eq ($gitExe.Substring(0,$gitExe.LastIndexOf("."))) }))
{
Start-Sleep 5
}
# Add bin folder to path
$env:PATH = "$WorkingDirectory\git\bin$( [IO.Path]::PathSeparator )" + $env:PATH
# Disable promopt for credential helper
Invoke-CustomCommand "git" @("config", "--system", "--unset", "credential.helper") | Write-Results
}
Function Invoke-CustomCommand
{
Param (
$commandPath,
$commandArguments,
$workingDir = (Get-Location),
$path = @(),
$envVars = @{ }
)
$path += $env:PATH
$newPath = $path -join [IO.Path]::PathSeparator
$pinfo = New-Object System.Diagnostics.ProcessStartInfo
$pinfo.FileName = $commandPath
$pinfo.WorkingDirectory = $workingDir
$pinfo.RedirectStandardError = $true
$pinfo.RedirectStandardOutput = $true
$pinfo.UseShellExecute = $false
$pinfo.Arguments = $commandArguments
$pinfo.EnvironmentVariables["PATH"] = $newPath
foreach ($env in $envVars.Keys)
{
Write-Verbose "Setting $env to $( $envVars.$env )"
$pinfo.EnvironmentVariables.Add($env,$envVars.$env.ToString())
}
$p = New-Object System.Diagnostics.Process
$p.StartInfo = $pinfo
$p.Start() | Out-Null
# Capture output during process execution so we don't hang
# if there is too much output.
# Microsoft documents a C# solution here:
# https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.processstartinfo.redirectstandardoutput?view=net-7.0&redirectedfrom=MSDN#remarks
# This code is based on https://stackoverflow.com/a/74748844
$stdOut = [System.Text.StringBuilder]::new()
$stdErr = [System.Text.StringBuilder]::new()
do
{
if (!$p.StandardOutput.EndOfStream)
{
$stdOut.AppendLine($p.StandardOutput.ReadLine())
}
if (!$p.StandardError.EndOfStream)
{
$stdErr.AppendLine($p.StandardError.ReadLine())
}
Start-Sleep -Milliseconds 10
}
while (-not $p.HasExited)
# Capture any standard output generated between our last poll and process end.
while (!$p.StandardOutput.EndOfStream)
{
$stdOut.AppendLine($p.StandardOutput.ReadLine())
}
# Capture any error output generated between our last poll and process end.
while (!$p.StandardError.EndOfStream)
{
$stdErr.AppendLine($p.StandardError.ReadLine())
}
$p.WaitForExit()
$executionResults = [pscustomobject]@{
StdOut = $stdOut.ToString()
StdErr = $stdErr.ToString()
ExitCode = $p.ExitCode
}
return $executionResults
}
function Write-Results
{
[cmdletbinding()]
param (
[Parameter(Mandatory = $True, ValuefromPipeline = $True)]
$results
)
if (![String]::IsNullOrWhiteSpace($results.StdOut))
{
Write-Verbose $results.StdOut
}
if (![String]::IsNullOrWhiteSpace($results.StdErr))
{
Write-Verbose $results.StdErr
}
}
function Format-StringAsNullOrTrimmed {
[cmdletbinding()]
param (
[Parameter(ValuefromPipeline=$True)]
$input
)
if ([string]::IsNullOrWhitespace($input)) {
return $null
}
return $input.Trim()
}
function Write-TerraformBackend
{
Set-Content -Path 'backend.tf' -Value @"
terraform {
backend "s3" {}
required_providers {
octopusdeploy = { source = "OctopusDeployLabs/octopusdeploy", version = ">= 0.21.1" }
}
}
"@
}
function Set-GitContactDetails
{
$gitEmail = Invoke-CustomCommand "git" @("config", "--global", "user.email", "octopus@octopus.com")
Write-Results $gitEmail
if (-not $gitEmail.ExitCode -eq 0)
{
Write-Error "Failed to set the git email address (exit code was $( $gitEmail.ExitCode ))."
}
$gitUser = Invoke-CustomCommand "git" @("config", "--global", "user.name", "Octopus Server")
Write-Results $gitUser
if (-not $gitUser.ExitCode -eq 0)
{
Write-Error "Failed to set the git name (exit code was $( $gitUser.ExitCode ))."
}
}
$spaceFilter = if (-not [string]::IsNullOrWhitespace($OctopusParameters["FindConflicts.Octopus.Spaces"]))
{
$OctopusParameters["FindConflicts.Octopus.Spaces"].Split("`n")
}
else
{
@()
}
$projectFilter = if (-not [string]::IsNullOrWhitespace($OctopusParameters["FindConflicts.Octopus.Projects"]))
{
$OctopusParameters["FindConflicts.Octopus.Projects"].Split("`n")
}
else
{
@()
}
$username = $OctopusParameters["FindConflicts.Git.Credentials.Username"]
$password = $OctopusParameters["FindConflicts.Git.Credentials.Password"]
$protocol = $OctopusParameters["FindConflicts.Git.Url.Protocol"]
$gitHost = $OctopusParameters["FindConflicts.Git.Url.Host"]
$org = $OctopusParameters["FindConflicts.Git.Url.Organization"]
$repo = $OctopusParameters["FindConflicts.Git.Url.Template"]
$region = $OctopusParameters["FindConflicts.Terraform.Backend.S3Region"]
$key = $OctopusParameters["FindConflicts.Terraform.Backend.S3Key"]
$bucket = $OctopusParameters["FindConflicts.Terraform.Backend.S3Bucket"]
if ([string]::IsNullOrWhitespace($username))
{
Write-Error "The FindConflicts.Git.Credentials.Username variable must be provided"
}
if ([string]::IsNullOrWhitespace($password))
{
Write-Error "The FindConflicts.Git.Credentials.Password variable must be provided"
}
if ( [string]::IsNullOrWhitespace($protocol))
{
Write-Error "The FindConflicts.Git.Url.Protocol variable must be defined."
}
if ( [string]::IsNullOrWhitespace($gitHost))
{
Write-Error "The FindConflicts.Git.Url.Host variable must be defined."
}
if ( [string]::IsNullOrWhitespace($repo))
{
Write-Error "The FindConflicts.Git.Url.Template variable must be defined."
}
if ( [string]::IsNullOrWhitespace($region))
{
Write-Error "The FindConflicts.Terraform.Backend.S3Region variable must be defined."
}
if ( [string]::IsNullOrWhitespace($key))
{
Write-Error "The FindConflicts.Terraform.Backend.S3Key variable must be defined."
}
if ( [string]::IsNullOrWhitespace($bucket))
{
Write-Error "The FindConflicts.Terraform.Backend.S3Bucket variable must be defined."
}
$templateRepoUrl = $protocol + "://" + $gitHost + "/" + $org + "/" + $repo + ".git"
$templateRepo = $protocol + "://" + $username + ":" + $password + "@" + $gitHost + "/" + $org + "/" + $repo + ".git"
$branch = "main"
# Check to see if it's Windows
if ($IsWindows -and $OctopusParameters['Octopus.Workerpool.Name'] -eq "Hosted Windows")
{
# Dynamic worker don't have git, download portable version and add to path for execution
Write-Host "Detected usage of Windows Dynamic Worker ..."
Get-GitExecutable -WorkingDirectory $PWD
}
Write-TerraformBackend
Set-GitContactDetails
Invoke-CustomCommand "terraform" @("init", "-no-color", "-backend-config=`"bucket=$bucket`"", "-backend-config=`"region=$region`"", "-backend-config=`"key=$key`"") | Write-Results
Write-Host "Verbose logs contain instructions for resolving merge conflicts."
$workspaces = Invoke-CustomCommand "terraform" @("workspace", "list")
Write-Results $workspaces
$parsedWorkspaces = $workspaces.StdOut.Replace("*", "").Split("`n")
foreach ($workspace in $parsedWorkspaces)
{
$trimmedWorkspace = $workspace | Format-StringAsNullOrTrimmed
if ($trimmedWorkspace -eq "default" -or [string]::IsNullOrWhitespace($trimmedWorkspace))
{
continue
}
Write-Verbose "Processing workspace $trimmedWorkspace"
Invoke-CustomCommand "terraform" @("workspace", "select", $trimmedWorkspace) | Write-Results
$state = Invoke-CustomCommand "terraform" @("show", "-json")
# state might include sensitive values, so don't print it unless there was an error
if (-not $state.ExitCode -eq 0)
{
Write-Results $state
continue
}
$parsedState = $state.StdOut | ConvertFrom-Json
$resources = $parsedState.values.root_module.resources | Where-Object {
$_.type -eq "octopusdeploy_project"
}
# The outputs allow us to contact the downstream instance)
$spaceName = (Invoke-CustomCommand "terraform" @("output", "-raw", "octopus_space_name")).StdOut | Format-StringAsNullOrTrimmed
foreach ($resource in $resources)
{
$url = $resource.values.git_library_persistence_settings.url | Format-StringAsNullOrTrimmed
$name = $resource.values.name | Format-StringAsNullOrTrimmed
# Optional filtering
if (-not($spaceFilter.Count -eq 0 -or $spaceFilter.Contains($spaceName)))
{
continue
}
if (-not($projectFilter.Count -eq 0 -or $projectFilter.Contains($name)))
{
continue
}
if (-not [string]::IsNullOrWhitespace($url))
{
mkdir $trimmedWorkspace | Out-Null
$parsedUrl = [System.Uri]$url
$urlWithCreds = $parsedUrl.Scheme + "://" + $username + ":" + $password + "@" + $parsedUrl.Host + ":" + $parsedUrl.Port + $parsedUrl.AbsolutePath
Write-Verbose "Cloning repo"
$cloneRepo = Invoke-CustomCommand "git" @("clone", $urlWithCreds, $trimmedWorkspace)
Write-Results $cloneRepo
if (-not $cloneRepo.ExitCode -eq 0)
{
Write-Error "Failed to clone repo (exit code was $( $cloneRepo.ExitCode ))."
}
Write-Verbose "Cloning upstream remote"
$addRemote = Invoke-CustomCommand "git" @("remote", 'add', 'upstream', $templateRepo) $trimmedWorkspace
Write-Results $addRemote
if (-not $addRemote.ExitCode -eq 0)
{
Write-Error "Failed to clone repo (exit code was $( $addRemote.ExitCode ))."
}
Write-Verbose "Fetching all"
$fetchAll = Invoke-CustomCommand "git" @("fetch", "--all") $trimmedWorkspace
Write-Results $fetchAll
if (-not $fetchAll.ExitCode -eq 0)
{
Write-Error "Failed to fetch all (exit code was $( $fetchAll.ExitCode ))."
}
Write-Verbose "Checking out upstream-$branch upstream/$branch"
$checkoutUpstream = Invoke-CustomCommand "git" @("checkout", "-b", "upstream-$branch", "upstream/$branch") $trimmedWorkspace
Write-Results $checkoutUpstream
if (-not $checkoutUpstream.ExitCode -eq 0)
{
Write-Error "Failed to checkout upstream (exit code was $( $checkoutUpstream.ExitCode ))."
}
if (-not($branch -eq "master" -or $branch -eq "main"))
{
Write-Verbose "Checking out $branch origin/$branch"
$checkoutDownstream = Invoke-CustomCommand "git" @("checkout", "-b", $branch, "origin/$branch") $trimmedWorkspace
}
else
{
Write-Verbose "Checking out $branch"
$checkoutDownstream = Invoke-CustomCommand "git" @("checkout", $branch) $trimmedWorkspace
}
Write-Results $checkoutDownstream
if (-not $checkoutDownstream.ExitCode -eq 0)
{
Write-Error "Failed to checkout downstream (exit code was $( $checkoutDownstream.ExitCode ))."
}
Write-Verbose "Merge base"
$mergeBase = Invoke-CustomCommand "git" @("merge-base", $branch, "upstream-$branch") $trimmedWorkspace
Write-Results $mergeBase
if (-not $mergeBase.ExitCode -eq 0)
{
Write-Error "Failed to merge base (exit code was $( $mergeBase.ExitCode ))."
}
Write-Verbose "Rev parse"
$mergeSourceCurrentCommit = Invoke-CustomCommand "git" @("rev-parse", "upstream-$branch") $trimmedWorkspace
Write-Results $mergeSourceCurrentCommit
if (-not $mergeSourceCurrentCommit.ExitCode -eq 0)
{
Write-Error "Failed to rev parse (exit code was $( $mergeSourceCurrentCommit.ExitCode ))."
}
Write-Verbose "Merge (no commit)"
$mergeResult = Invoke-CustomCommand "git" @("merge", "--no-commit", "--no-ff", "upstream-$branch") $trimmedWorkspace
Write-Results $mergeResult
if ($mergeBase.StdOut -eq $mergeSourceCurrentCommit.StdOut)
{
Write-Host "No changes found in the upstream repo $templateRepoUrl that do not exist in the downstream repo $url for project `"$name`" in space $spaceName"
}
elseif (-not $mergeResult.ExitCode -eq 0)
{
Write-Warning "Changes between upstream repo $templateRepoUrl conflict with changes in downstream repo $url for project `"$name`" in space $spaceName."
Write-Verbose "To resolve the conflicts, run the following commands:"
Write-Verbose "mkdir cac"
Write-Verbose "cd cac"
Write-Verbose "git clone $url ."
Write-Verbose "git remote add upstream $templateRepoUrl"
Write-Verbose "git fetch --all"
Write-Verbose "git checkout -b upstream-$branch upstream/$branch"
if (-not($branch -eq "master" -or $branch -eq "main"))
{
Write-Verbose "git checkout -b $branch origin/$branch"
}
else
{
Write-Verbose "git checkout $branch"
Write-Verbose "git merge-base $branch upstream-$branch"
Write-Verbose "git merge --no-commit --no-ff upstream-$branch"
}
}
else
{
# https://stackoverflow.com/a/76272919
# How to commit a merge non-interactively
Write-Verbose "Git commit"
$mergeContinue = Invoke-CustomCommand "git" @("commit", "--no-edit") $trimmedWorkspace
Write-Results $mergeContinue
if (-not $mergeContinue.ExitCode -eq 0)
{
Write-Error "Failed to merge continue (exit code was $( $mergeContinue.ExitCode ))."
}
$diffResult = Invoke-CustomCommand "git" @("diff", "--quiet", "--exit-code", "@{upstream}") $trimmedWorkspace
Write-Results $diffResult
if (-not $diffResult.ExitCode -eq 0)
{
$pushResult = Invoke-CustomCommand "git" @("push", "origin") $trimmedWorkspace
Write-Results $pushResult
if ($pushResult.ExitCode -eq 0)
{
Write-Host "Changes were merged between upstream repo $templateRepoUrl and downstream repo $url for project `"$name`" in space $spaceName."
}
else
{
Write-Warning "Failed to push changes to downstream repo $url for project `"$name`" in space $spaceName (exit code $( $pushResult.ExitCode ))."
}
}
else
{
Write-Host "No changes found in the upstream repo $templateRepoUrl that do not exist in the downstream repo $url for project `"$name`" in space $spaceName"
}
}
}
else
{
Write-Verbose "`"$name`" is not a CaC project"
}
}
}
Provided under the Apache License version 2.0.
To use this template in Octopus Deploy, copy the JSON below and paste it into the Library → Step templates → Import dialog.
{
"Id": "c2536053-024f-499e-bf14-7e55c5a675d0",
"Name": "Octopus - Merge CaC Updates (S3 Backend)",
"Description": "This step queries each workspace in the Terraform state for downstream Octopus CaC enabled projects, extracts the Git repo associated with the CaC project, and merges any changes so long as there are no merge conflicts.\n\nIf there is a merge conflict between the upstream and downstream repos, instructions for manually resolving the conflict are provided.",
"Version": 4,
"ExportedAt": "2023-11-17T01:19:30.474Z",
"ActionType": "Octopus.AwsRunScript",
"Author": "mcasperson",
"Packages": [],
"Parameters": [
{
"Id": "5abfbf98-22f0-47a2-8dd8-f16e2227d4ba",
"Name": "FindConflicts.Octopus.Spaces",
"Label": "Octopus Spaces",
"HelpText": "An optional newline-separated list of space names with projects to merge changes into. Leave this field blank to merge changes to projects in all spaces.",
"DefaultValue": "",
"DisplaySettings": {
"Octopus.ControlType": "MultiLineText"
}
},
{
"Id": "56b85050-7c06-4a84-9565-88730a4859dc",
"Name": "FindConflicts.Octopus.Projects",
"Label": "Octopus Projects",
"HelpText": "A newline-separated list of projects to merge changes into. Leave this field blank to merge changes to all projects.",
"DefaultValue": "",
"DisplaySettings": {
"Octopus.ControlType": "MultiLineText"
}
},
{
"Id": "539dc165-d880-4bf2-99c2-7024f36f7593",
"Name": "FindConflicts.Git.Credentials.Username",
"Label": "Git Username",
"HelpText": "The git repo username. When using GitHub with an access token, the value is `x-access-token`.",
"DefaultValue": "x-access-token",
"DisplaySettings": {
"Octopus.ControlType": "SingleLineText"
}
},
{
"Id": "7cf2352e-7381-485d-994c-ff128ee0fe8b",
"Name": "FindConflicts.Git.Credentials.Password",
"Label": "Git Password",
"HelpText": "The git repo password or access token.",
"DefaultValue": "",
"DisplaySettings": {
"Octopus.ControlType": "Sensitive"
}
},
{
"Id": "3887173c-a7f9-4311-848f-6f82736df2ce",
"Name": "FindConflicts.Git.Url.Protocol",
"Label": "Git Protocol",
"HelpText": "The git repo protocol.",
"DefaultValue": "https",
"DisplaySettings": {
"Octopus.ControlType": "Select",
"Octopus.SelectOptions": "https|HTTPS\nhttp|HTTP"
}
},
{
"Id": "0781245e-3f02-4d5b-a2ad-68c5f6270cb5",
"Name": "FindConflicts.Git.Url.Host",
"Label": "Git Hostname",
"HelpText": "The git repo host name.",
"DefaultValue": "github.com",
"DisplaySettings": {
"Octopus.ControlType": "SingleLineText"
}
},
{
"Id": "7891f539-752e-4933-abcc-68db8ba9d114",
"Name": "FindConflicts.Git.Url.Organization",
"Label": "Git Organization",
"HelpText": "The git repo owner or organization i.e. `owner` in the url `https://github.com/owner/repo`.",
"DefaultValue": "",
"DisplaySettings": {
"Octopus.ControlType": "SingleLineText"
}
},
{
"Id": "e5a6f5a3-fec6-47dd-ad46-82ed7df409f7",
"Name": "FindConflicts.Git.Url.Template",
"Label": "Git Template Repo",
"HelpText": "The repo holding the upstream, or template, CaC project i.e. `repo` in the url `https://github.com/owner/repo`.",
"DefaultValue": "",
"DisplaySettings": {
"Octopus.ControlType": "SingleLineText"
}
},
{
"Id": "d8abeacf-b392-4a9e-b95f-50d0d033819f",
"Name": "FindConflicts.Terraform.Backend.S3Region",
"Label": "AWS Region",
"HelpText": "The AWS region hosting the S3 bucket persisting the Terraform state.",
"DefaultValue": "",
"DisplaySettings": {
"Octopus.ControlType": "SingleLineText"
}
},
{
"Id": "c9f76f2e-9c8b-4af2-9768-6449526d52ca",
"Name": "FindConflicts.Terraform.Backend.S3Key",
"Label": "S3 Key",
"HelpText": "The name of the file in the S3 bucket hosting the Terraform state.",
"DefaultValue": "Project_#{Octopus.Project.Name | Replace \"[^A-Za-z0-9]\" \"_\"}",
"DisplaySettings": {
"Octopus.ControlType": "SingleLineText"
}
},
{
"Id": "fa261df0-01dc-4310-ad1e-1b07c3145e41",
"Name": "FindConflicts.Terraform.Backend.S3Bucket",
"Label": "S3 Bucket",
"HelpText": "The name of the S3 bucket hosting the Terraform state.",
"DefaultValue": "",
"DisplaySettings": {
"Octopus.ControlType": "SingleLineText"
}
},
{
"Id": "00d184f8-287f-46e4-8848-282138484cfa",
"Name": "FindConflicts.Terraform.Aws.Account",
"Label": "AWS Account",
"HelpText": "The AWS account used to access the S3 bucket.",
"DefaultValue": "",
"DisplaySettings": {
"Octopus.ControlType": "AmazonWebServicesAccount"
}
}
],
"Properties": {
"Octopus.Action.Script.ScriptSource": "Inline",
"Octopus.Action.Script.Syntax": "PowerShell",
"Octopus.Action.Aws.AssumeRole": "False",
"Octopus.Action.AwsAccount.UseInstanceRole": "False",
"OctopusUseBundledTooling": "False",
"Octopus.Action.Script.ScriptBody": "# Check to see if $IsWindows is available\nif ($null -eq $IsWindows)\n{\n Write-Host \"Determining Operating System...\"\n $IsWindows = ([System.Environment]::OSVersion.Platform -eq \"Win32NT\")\n $IsLinux = ([System.Environment]::OSVersion.Platform -eq \"Unix\")\n}\n\nFunction Get-GitExecutable\n{\n # Define parameters\n param (\n $WorkingDirectory\n )\n\n # Define variables\n $gitExe = \"PortableGit-2.41.0.3-64-bit.7z.exe\"\n $gitDownloadUrl = \"https://github.com/git-for-windows/git/releases/download/v2.41.0.windows.3/$gitExe\"\n $gitDownloadArguments = @{ }\n $gitDownloadArguments.Add(\"Uri\", $gitDownloadUrl)\n $gitDownloadArguments.Add(\"OutFile\", \"$WorkingDirectory/git/$gitExe\")\n\n # This makes downloading faster\n $ProgressPreference = 'SilentlyContinue'\n\n # Check to see if git subfolder exists\n if ((Test-Path -Path \"$WorkingDirectory/git\") -eq $false)\n {\n # Create subfolder\n New-Item -Path \"$WorkingDirectory/git\" -ItemType Directory | Out-Null\n }\n\n # Check PowerShell version\n if ($PSVersionTable.PSVersion.Major -lt 6)\n {\n # Use basic parsing is required\n $gitDownloadArguments.Add(\"UseBasicParsing\", $true)\n }\n\n # Download Git\n Write-Host \"Downloading Git ...\"\n Invoke-WebRequest @gitDownloadArguments\n\n # Extract Git\n $gitExtractArguments = @()\n $gitExtractArguments += \"-o\"\n $gitExtractArguments += \"$WorkingDirectory\\git\"\n $gitExtractArguments += \"-y\"\n $gitExtractArguments += \"-bd\"\n\n Write-Host \"Extracting Git download ...\"\n & \"$WorkingDirectory\\git\\$gitExe\" $gitExtractArguments\n\n # Wait until unzip action is complete\n while ($null -ne (Get-Process | Where-Object { $_.ProcessName -eq ($gitExe.Substring(0,$gitExe.LastIndexOf(\".\"))) }))\n {\n Start-Sleep 5\n }\n\n # Add bin folder to path\n $env:PATH = \"$WorkingDirectory\\git\\bin$( [IO.Path]::PathSeparator )\" + $env:PATH\n\n # Disable promopt for credential helper\n Invoke-CustomCommand \"git\" @(\"config\", \"--system\", \"--unset\", \"credential.helper\") | Write-Results\n}\n\nFunction Invoke-CustomCommand\n{\n Param (\n $commandPath,\n $commandArguments,\n $workingDir = (Get-Location),\n $path = @(),\n $envVars = @{ }\n )\n\n $path += $env:PATH\n $newPath = $path -join [IO.Path]::PathSeparator\n\n $pinfo = New-Object System.Diagnostics.ProcessStartInfo\n $pinfo.FileName = $commandPath\n $pinfo.WorkingDirectory = $workingDir\n $pinfo.RedirectStandardError = $true\n $pinfo.RedirectStandardOutput = $true\n $pinfo.UseShellExecute = $false\n $pinfo.Arguments = $commandArguments\n $pinfo.EnvironmentVariables[\"PATH\"] = $newPath\n\n foreach ($env in $envVars.Keys)\n {\n Write-Verbose \"Setting $env to $( $envVars.$env )\"\n $pinfo.EnvironmentVariables.Add($env,$envVars.$env.ToString())\n }\n\n $p = New-Object System.Diagnostics.Process\n $p.StartInfo = $pinfo\n $p.Start() | Out-Null\n\n # Capture output during process execution so we don't hang\n # if there is too much output.\n # Microsoft documents a C# solution here:\n # https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.processstartinfo.redirectstandardoutput?view=net-7.0&redirectedfrom=MSDN#remarks\n # This code is based on https://stackoverflow.com/a/74748844\n $stdOut = [System.Text.StringBuilder]::new()\n $stdErr = [System.Text.StringBuilder]::new()\n do\n {\n if (!$p.StandardOutput.EndOfStream)\n {\n $stdOut.AppendLine($p.StandardOutput.ReadLine())\n }\n if (!$p.StandardError.EndOfStream)\n {\n $stdErr.AppendLine($p.StandardError.ReadLine())\n }\n\n Start-Sleep -Milliseconds 10\n }\n while (-not $p.HasExited)\n\n # Capture any standard output generated between our last poll and process end.\n while (!$p.StandardOutput.EndOfStream)\n {\n $stdOut.AppendLine($p.StandardOutput.ReadLine())\n }\n\n # Capture any error output generated between our last poll and process end.\n while (!$p.StandardError.EndOfStream)\n {\n $stdErr.AppendLine($p.StandardError.ReadLine())\n }\n\n $p.WaitForExit()\n\n $executionResults = [pscustomobject]@{\n StdOut = $stdOut.ToString()\n StdErr = $stdErr.ToString()\n ExitCode = $p.ExitCode\n }\n\n return $executionResults\n\n}\n\nfunction Write-Results\n{\n [cmdletbinding()]\n param (\n [Parameter(Mandatory = $True, ValuefromPipeline = $True)]\n $results\n )\n\n if (![String]::IsNullOrWhiteSpace($results.StdOut))\n {\n Write-Verbose $results.StdOut\n }\n\n if (![String]::IsNullOrWhiteSpace($results.StdErr))\n {\n Write-Verbose $results.StdErr\n }\n}\n\nfunction Format-StringAsNullOrTrimmed {\n [cmdletbinding()]\n param (\n [Parameter(ValuefromPipeline=$True)]\n $input\n )\n\n if ([string]::IsNullOrWhitespace($input)) {\n return $null\n }\n\n return $input.Trim()\n}\n\nfunction Write-TerraformBackend\n{\n Set-Content -Path 'backend.tf' -Value @\"\nterraform {\n backend \"s3\" {}\n required_providers {\n octopusdeploy = { source = \"OctopusDeployLabs/octopusdeploy\", version = \">= 0.21.1\" }\n }\n }\n\"@\n}\n\nfunction Set-GitContactDetails\n{\n $gitEmail = Invoke-CustomCommand \"git\" @(\"config\", \"--global\", \"user.email\", \"octopus@octopus.com\")\n Write-Results $gitEmail\n if (-not $gitEmail.ExitCode -eq 0)\n {\n Write-Error \"Failed to set the git email address (exit code was $( $gitEmail.ExitCode )).\"\n }\n\n $gitUser = Invoke-CustomCommand \"git\" @(\"config\", \"--global\", \"user.name\", \"Octopus Server\")\n Write-Results $gitUser\n if (-not $gitUser.ExitCode -eq 0)\n {\n Write-Error \"Failed to set the git name (exit code was $( $gitUser.ExitCode )).\"\n }\n}\n\n$spaceFilter = if (-not [string]::IsNullOrWhitespace($OctopusParameters[\"FindConflicts.Octopus.Spaces\"]))\n{\n $OctopusParameters[\"FindConflicts.Octopus.Spaces\"].Split(\"`n\")\n}\nelse\n{\n @()\n}\n$projectFilter = if (-not [string]::IsNullOrWhitespace($OctopusParameters[\"FindConflicts.Octopus.Projects\"]))\n{\n $OctopusParameters[\"FindConflicts.Octopus.Projects\"].Split(\"`n\")\n}\nelse\n{\n @()\n}\n$username = $OctopusParameters[\"FindConflicts.Git.Credentials.Username\"]\n$password = $OctopusParameters[\"FindConflicts.Git.Credentials.Password\"]\n$protocol = $OctopusParameters[\"FindConflicts.Git.Url.Protocol\"]\n$gitHost = $OctopusParameters[\"FindConflicts.Git.Url.Host\"]\n$org = $OctopusParameters[\"FindConflicts.Git.Url.Organization\"]\n$repo = $OctopusParameters[\"FindConflicts.Git.Url.Template\"]\n$region = $OctopusParameters[\"FindConflicts.Terraform.Backend.S3Region\"]\n$key = $OctopusParameters[\"FindConflicts.Terraform.Backend.S3Key\"]\n$bucket = $OctopusParameters[\"FindConflicts.Terraform.Backend.S3Bucket\"]\n\nif ([string]::IsNullOrWhitespace($username))\n{\n Write-Error \"The FindConflicts.Git.Credentials.Username variable must be provided\"\n}\n\nif ([string]::IsNullOrWhitespace($password))\n{\n Write-Error \"The FindConflicts.Git.Credentials.Password variable must be provided\"\n}\n\nif ( [string]::IsNullOrWhitespace($protocol))\n{\n Write-Error \"The FindConflicts.Git.Url.Protocol variable must be defined.\"\n}\n\nif ( [string]::IsNullOrWhitespace($gitHost))\n{\n Write-Error \"The FindConflicts.Git.Url.Host variable must be defined.\"\n}\n\nif ( [string]::IsNullOrWhitespace($repo))\n{\n Write-Error \"The FindConflicts.Git.Url.Template variable must be defined.\"\n}\n\nif ( [string]::IsNullOrWhitespace($region))\n{\n Write-Error \"The FindConflicts.Terraform.Backend.S3Region variable must be defined.\"\n}\n\nif ( [string]::IsNullOrWhitespace($key))\n{\n Write-Error \"The FindConflicts.Terraform.Backend.S3Key variable must be defined.\"\n}\n\nif ( [string]::IsNullOrWhitespace($bucket))\n{\n Write-Error \"The FindConflicts.Terraform.Backend.S3Bucket variable must be defined.\"\n}\n\n$templateRepoUrl = $protocol + \"://\" + $gitHost + \"/\" + $org + \"/\" + $repo + \".git\"\n$templateRepo = $protocol + \"://\" + $username + \":\" + $password + \"@\" + $gitHost + \"/\" + $org + \"/\" + $repo + \".git\"\n$branch = \"main\"\n\n# Check to see if it's Windows\nif ($IsWindows -and $OctopusParameters['Octopus.Workerpool.Name'] -eq \"Hosted Windows\")\n{\n # Dynamic worker don't have git, download portable version and add to path for execution\n Write-Host \"Detected usage of Windows Dynamic Worker ...\"\n Get-GitExecutable -WorkingDirectory $PWD\n}\n\nWrite-TerraformBackend\nSet-GitContactDetails\n\nInvoke-CustomCommand \"terraform\" @(\"init\", \"-no-color\", \"-backend-config=`\"bucket=$bucket`\"\", \"-backend-config=`\"region=$region`\"\", \"-backend-config=`\"key=$key`\"\") | Write-Results\n\nWrite-Host \"Verbose logs contain instructions for resolving merge conflicts.\"\n\n$workspaces = Invoke-CustomCommand \"terraform\" @(\"workspace\", \"list\")\n\nWrite-Results $workspaces\n\n$parsedWorkspaces = $workspaces.StdOut.Replace(\"*\", \"\").Split(\"`n\")\n\nforeach ($workspace in $parsedWorkspaces)\n{\n $trimmedWorkspace = $workspace | Format-StringAsNullOrTrimmed\n\n if ($trimmedWorkspace -eq \"default\" -or [string]::IsNullOrWhitespace($trimmedWorkspace))\n {\n continue\n }\n\n Write-Verbose \"Processing workspace $trimmedWorkspace\"\n\n Invoke-CustomCommand \"terraform\" @(\"workspace\", \"select\", $trimmedWorkspace) | Write-Results\n\n $state = Invoke-CustomCommand \"terraform\" @(\"show\", \"-json\")\n\n # state might include sensitive values, so don't print it unless there was an error\n\n if (-not $state.ExitCode -eq 0)\n {\n Write-Results $state\n continue\n }\n\n $parsedState = $state.StdOut | ConvertFrom-Json\n\n $resources = $parsedState.values.root_module.resources | Where-Object {\n $_.type -eq \"octopusdeploy_project\"\n }\n\n # The outputs allow us to contact the downstream instance)\n $spaceName = (Invoke-CustomCommand \"terraform\" @(\"output\", \"-raw\", \"octopus_space_name\")).StdOut | Format-StringAsNullOrTrimmed\n\n foreach ($resource in $resources)\n {\n $url = $resource.values.git_library_persistence_settings.url | Format-StringAsNullOrTrimmed\n $name = $resource.values.name | Format-StringAsNullOrTrimmed\n\n # Optional filtering\n if (-not($spaceFilter.Count -eq 0 -or $spaceFilter.Contains($spaceName)))\n {\n continue\n }\n\n if (-not($projectFilter.Count -eq 0 -or $projectFilter.Contains($name)))\n {\n continue\n }\n\n if (-not [string]::IsNullOrWhitespace($url))\n {\n mkdir $trimmedWorkspace | Out-Null\n\n $parsedUrl = [System.Uri]$url\n $urlWithCreds = $parsedUrl.Scheme + \"://\" + $username + \":\" + $password + \"@\" + $parsedUrl.Host + \":\" + $parsedUrl.Port + $parsedUrl.AbsolutePath\n\n Write-Verbose \"Cloning repo\"\n $cloneRepo = Invoke-CustomCommand \"git\" @(\"clone\", $urlWithCreds, $trimmedWorkspace)\n Write-Results $cloneRepo\n if (-not $cloneRepo.ExitCode -eq 0)\n {\n Write-Error \"Failed to clone repo (exit code was $( $cloneRepo.ExitCode )).\"\n }\n\n Write-Verbose \"Cloning upstream remote\"\n $addRemote = Invoke-CustomCommand \"git\" @(\"remote\", 'add', 'upstream', $templateRepo) $trimmedWorkspace\n Write-Results $addRemote\n if (-not $addRemote.ExitCode -eq 0)\n {\n Write-Error \"Failed to clone repo (exit code was $( $addRemote.ExitCode )).\"\n }\n\n Write-Verbose \"Fetching all\"\n $fetchAll = Invoke-CustomCommand \"git\" @(\"fetch\", \"--all\") $trimmedWorkspace\n Write-Results $fetchAll\n if (-not $fetchAll.ExitCode -eq 0)\n {\n Write-Error \"Failed to fetch all (exit code was $( $fetchAll.ExitCode )).\"\n }\n\n Write-Verbose \"Checking out upstream-$branch upstream/$branch\"\n $checkoutUpstream = Invoke-CustomCommand \"git\" @(\"checkout\", \"-b\", \"upstream-$branch\", \"upstream/$branch\") $trimmedWorkspace\n Write-Results $checkoutUpstream\n if (-not $checkoutUpstream.ExitCode -eq 0)\n {\n Write-Error \"Failed to checkout upstream (exit code was $( $checkoutUpstream.ExitCode )).\"\n }\n\n if (-not($branch -eq \"master\" -or $branch -eq \"main\"))\n {\n Write-Verbose \"Checking out $branch origin/$branch\"\n $checkoutDownstream = Invoke-CustomCommand \"git\" @(\"checkout\", \"-b\", $branch, \"origin/$branch\") $trimmedWorkspace\n }\n else\n {\n Write-Verbose \"Checking out $branch\"\n $checkoutDownstream = Invoke-CustomCommand \"git\" @(\"checkout\", $branch) $trimmedWorkspace\n }\n\n Write-Results $checkoutDownstream\n if (-not $checkoutDownstream.ExitCode -eq 0)\n {\n Write-Error \"Failed to checkout downstream (exit code was $( $checkoutDownstream.ExitCode )).\"\n }\n\n Write-Verbose \"Merge base\"\n $mergeBase = Invoke-CustomCommand \"git\" @(\"merge-base\", $branch, \"upstream-$branch\") $trimmedWorkspace\n Write-Results $mergeBase\n if (-not $mergeBase.ExitCode -eq 0)\n {\n Write-Error \"Failed to merge base (exit code was $( $mergeBase.ExitCode )).\"\n }\n\n Write-Verbose \"Rev parse\"\n $mergeSourceCurrentCommit = Invoke-CustomCommand \"git\" @(\"rev-parse\", \"upstream-$branch\") $trimmedWorkspace\n Write-Results $mergeSourceCurrentCommit\n if (-not $mergeSourceCurrentCommit.ExitCode -eq 0)\n {\n Write-Error \"Failed to rev parse (exit code was $( $mergeSourceCurrentCommit.ExitCode )).\"\n }\n\n Write-Verbose \"Merge (no commit)\"\n $mergeResult = Invoke-CustomCommand \"git\" @(\"merge\", \"--no-commit\", \"--no-ff\", \"upstream-$branch\") $trimmedWorkspace\n Write-Results $mergeResult\n\n if ($mergeBase.StdOut -eq $mergeSourceCurrentCommit.StdOut)\n {\n Write-Host \"No changes found in the upstream repo $templateRepoUrl that do not exist in the downstream repo $url for project `\"$name`\" in space $spaceName\"\n }\n elseif (-not $mergeResult.ExitCode -eq 0)\n {\n Write-Warning \"Changes between upstream repo $templateRepoUrl conflict with changes in downstream repo $url for project `\"$name`\" in space $spaceName.\"\n Write-Verbose \"To resolve the conflicts, run the following commands:\"\n Write-Verbose \"mkdir cac\"\n Write-Verbose \"cd cac\"\n Write-Verbose \"git clone $url .\"\n Write-Verbose \"git remote add upstream $templateRepoUrl\"\n Write-Verbose \"git fetch --all\"\n Write-Verbose \"git checkout -b upstream-$branch upstream/$branch\"\n if (-not($branch -eq \"master\" -or $branch -eq \"main\"))\n {\n Write-Verbose \"git checkout -b $branch origin/$branch\"\n }\n else\n {\n Write-Verbose \"git checkout $branch\"\n Write-Verbose \"git merge-base $branch upstream-$branch\"\n Write-Verbose \"git merge --no-commit --no-ff upstream-$branch\"\n }\n }\n else\n {\n # https://stackoverflow.com/a/76272919\n # How to commit a merge non-interactively\n Write-Verbose \"Git commit\"\n $mergeContinue = Invoke-CustomCommand \"git\" @(\"commit\", \"--no-edit\") $trimmedWorkspace\n Write-Results $mergeContinue\n if (-not $mergeContinue.ExitCode -eq 0)\n {\n Write-Error \"Failed to merge continue (exit code was $( $mergeContinue.ExitCode )).\"\n }\n\n $diffResult = Invoke-CustomCommand \"git\" @(\"diff\", \"--quiet\", \"--exit-code\", \"@{upstream}\") $trimmedWorkspace\n Write-Results $diffResult\n\n if (-not $diffResult.ExitCode -eq 0)\n {\n $pushResult = Invoke-CustomCommand \"git\" @(\"push\", \"origin\") $trimmedWorkspace\n Write-Results $pushResult\n\n if ($pushResult.ExitCode -eq 0)\n {\n Write-Host \"Changes were merged between upstream repo $templateRepoUrl and downstream repo $url for project `\"$name`\" in space $spaceName.\"\n }\n else\n {\n Write-Warning \"Failed to push changes to downstream repo $url for project `\"$name`\" in space $spaceName (exit code $( $pushResult.ExitCode )).\"\n }\n }\n else\n {\n Write-Host \"No changes found in the upstream repo $templateRepoUrl that do not exist in the downstream repo $url for project `\"$name`\" in space $spaceName\"\n }\n }\n }\n else\n {\n Write-Verbose \"`\"$name`\" is not a CaC project\"\n }\n }\n}",
"Octopus.Action.Aws.Region": "#{FindConflicts.Terraform.Backend.S3Region}",
"Octopus.Action.AwsAccount.Variable": "#{FindConflicts.Terraform.Aws.Account}"
},
"Category": "Octopus",
"HistoryUrl": "https://github.com/OctopusDeploy/Library/commits/master/step-templates//opt/buildagent/work/75443764cd38076d/step-templates/octopus-merge-cac-updates.json",
"Website": "/step-templates/c2536053-024f-499e-bf14-7e55c5a675d0",
"Logo": "iVBORw0KGgoAAAANSUhEUgAAAMgAAADICAMAAACahl6sAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAC1QTFRFT6Tl////L5Pg8vj9Y67omsvwPJrisdfzfbzs5fL7y+T32Ov5isLucLXqvt31CJPHWwAABMJJREFUeNrs3deW4jAMAFDF3U75/89dlp0ZhiU4blJEjvQ8hYubLJsA00UCBCIQgQhEIAIRiEAEIhCBCEQgAhGIQAQiEIEIhD8kJm+t+QprfdKfB9HbYpx6CWfspj8HMi+gMgHL/AmQA8W3JTKH+ALFvzCeL0RbpyoCPE9IJeNOSQwh5Z3qd6yRGWQ2qi2cZQWxqj1WzQYSjeoJmJlAklOd4VlArOqPhQEkqBERToeMcfRJBkC0Uep8CfBpjz4JsHJ0zF3dkEWNje0kiB/sUC6eApndaIiCMyAa1PiwJ0AWhRGJHJJQHG2dC7h1rNbO1QOxSA7lNCkkKrQIpJCAB1GREILYIC1NAiwbpKFJgGWDNExcwGstfExcZBCHC6nOglshHtmhViLIig1RNBCN7qjtW8C0Z1UvJcC1Z9XmwMBzzvobmgAyEzgq91dtEEsBsQSQQAFZCSBAATEEEApHZbrVBIkkEIUPSVeB+KtALA0kXQUSrwKZBCIQBnk8Y4i5CsReBeKvkqLM+BCSDWJlrZFvGk9SRTHshkgjZCGAaArIxm3H3grhVzFlW2msfl1ca79UJ1bofYvsDHHlNdTZnlh5MghuPd5NdBDUNZHyCkfktIh03XzALGRPlBDPac7qgWjHZzWcmF5zmmkhidMQ6boKiDXcDTUEaylZqCGJ0Vjvu/fLJtHqhSANEvqb2OYqkOUqEHuVMbJcZdZCGiPhKhC4yjqiIjEE7XThMp8fAWII3mY3kUIQD+AMKQTzPiBhgQ63HlT/KSvgtoi0dq5mCPah1UIE0eh3sT0NhOByvKeAkFzi8PgQomumFhsyOxpIzZN4gLOj5plVwNpR0b2AuePWKBEHQu24pSsJA+LVCeHHQxZ1SiyDIdqok8IOhSSnTottHEQTdyt4ettAj4KkzA4dMikk2Dht2S5ptm1vswnPDxn0YyDZ5oDM3iToo2T5voWaYe+Q+vdjH80QyAzZhCgcDtLMI1Tmtz9w++XHgziHQHJJu/OZ3bs9Xn8gQ72NcP3dKqEfkp10F51xhoIi2I91R+LurXV/5q7pH+wx061CzO16oSQleMyr8fXvwMA0Pro8432DPD/ySx8XrHfSuDAM8n6UhnjQabaiXf5Bq/lREHvEeNtn1rJ08+C/uXkQZHeguxAPC3UvtcJYUogLzZX5hhZZvS6onG5lxXtzWGaygwb79vT/IXhdlNibwlKYOR6T8xjI7W8n+xV7T+GH4tMzWwR+lZhRkJYSsC0thpmCYqyngOz3rN2FLBZ2wZflBCggUHF0Vnp88JKienzIXLSEZCZqU7IKr/gQW9yx3pzV7Y9kvWZWTRRIqDmTtRUnU7b2lLcTYmoqHqnmiO1poER0SPkAeZMAZxaJx0Y3TCdAclsIqDz03ALcyxfTCZBsthoGXWmigGyVhWPLFJJfuuKQWycoEFdXbH4dJJoJxNR1eD/kshz6yn48cF8yW8sFoitflB1w6Q8n+/15Za7oA17/pYNmYgP5fmWm8L1NOHPWgK8kuFew1/JXtOA0yJCv7ah7X8ObUuT5kObU30+fDZm8+zqP+HTIpK0xQ796b5Kv2hSIQAQiEIEIRCACEYhABCIQgQhEIAIRiEAEIpBf8UeAAQAEjtYmlDTcCgAAAABJRU5ErkJggg==",
"$Meta": {
"Type": "ActionTemplate"
}
}
Page updated on Friday, November 17, 2023