Octopus - Find CaC Updates (S3 Backend)

Octopus.AwsRunScript exported 2023-11-16 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 determines if there are any changes to merge into the downstream project from the upstream project.

This indicates if changes to an upstream project are available to be merged into a downstream project, either automatically, or after resolving merge conflicts.

Parameters

When steps based on the template are included in a project’s deployment process, the parameters below can be set.

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 = @()
    )

    $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
    $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 Write-TerraformBackend {
    Set-Content -Path 'backend.tf' -Value @"
terraform {
        backend "s3" {}
        required_providers {
          octopusdeploy = { source = "OctopusDeployLabs/octopusdeploy", version = "0.14.9" }
        }
    }
"@
}

function Format-StringAsNullOrTrimmed {
    [cmdletbinding()]
    param (
        [Parameter(ValuefromPipeline=$True)]
        $input
    )

    if ([string]::IsNullOrWhitespace($input)) {
        return $null
    }

    return $input.Trim()
}

$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"]

# Validate the inputs
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 provided"
}

if ([string]::IsNullOrWhitespace($gitHost)) {
    Write-Error "The FindConflicts.Git.Url.Host variable must be provided"
}

if ([string]::IsNullOrWhitespace($org)) {
    Write-Error "The FindConflicts.Git.Url.Organization variable must be provided"
}

if ([string]::IsNullOrWhitespace($repo)) {
    Write-Error "The FindConflicts.Git.Url.Template variable must be provided"
}

if ([string]::IsNullOrWhitespace($region)) {
    Write-Error "The FindConflicts.Terraform.Backend.S3Region variable must be provided"
}

if ([string]::IsNullOrWhitespace($key)) {
    Write-Error "The FindConflicts.Terraform.Backend.S3Key variable must be provided"
}

if ([string]::IsNullOrWhitespace($bucket)) {
    Write-Error "The FindConflicts.Terraform.Backend.S3Bucket variable must be provided"
}

$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

Invoke-CustomCommand "git" @("config", "--global", "user.email", "octopus@octopus.com") | Write-Results
Invoke-CustomCommand "git" @("config", "--global", "user.name", "Octopus Server") | Write-Results

Invoke-CustomCommand "terraform" @("init", "-no-color", "-backend-config=`"bucket=$bucket`"", "-backend-config=`"region=$region`"", "-backend-config=`"key=$key`"") | Write-Results

Write-Host "- Up to date"
Write-Host "> Can automatically merge"
Write-Host "× Merge conflict"
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")

$downstreamCount = 0
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)
    $spaceId = Invoke-CustomCommand "terraform" @("output", "-raw", "octopus_space_id")
    $spaceName = Invoke-CustomCommand "terraform" @("output", "-raw", "octopus_space_name")
    $space = if ([string]::IsNullOrWhitespace($spaceName.StdOut))
    {
        $spaceId.StdOut | Format-StringAsNullOrTrimmed
    }
    else
    {
        $spaceName.StdOut | Format-StringAsNullOrTrimmed
    }

    foreach ($resource in $resources)
    {
        $url = $resource.values.git_library_persistence_settings.url | Format-StringAsNullOrTrimmed
        $spaceId = $resource.values.space_id | Format-StringAsNullOrTrimmed
        $name = $resource.values.name | Format-StringAsNullOrTrimmed

        if (-not [string]::IsNullOrWhitespace($url))
        {
            $downstreamCount++

            mkdir $trimmedWorkspace | Out-Null

            Invoke-CustomCommand "git" @("clone", $url, $trimmedWorkspace) | Write-Results
            Invoke-CustomCommand "git" @("remote", 'add', 'upstream', $templateRepo) $trimmedWorkspace | Write-Results
            Invoke-CustomCommand "git" @("fetch", "--all") $trimmedWorkspace | Write-Results
            Invoke-CustomCommand "git" @("checkout", "-b", "upstream-$branch", "upstream/$branch") $trimmedWorkspace | Write-Results

            if (-not($branch -eq "master" -or $branch -eq "main"))
            {
                Invoke-CustomCommand "git" @("checkout", "-b", $branch, "origin/$branch") $trimmedWorkspace | Write-Results
            }
            else
            {
                Invoke-CustomCommand "git" @("checkout", $branch) $trimmedWorkspace | Write-Results
            }

            $mergeBase = Invoke-CustomCommand "git" @("merge-base", $branch, "upstream-$branch") $trimmedWorkspace

            Write-Results $mergeBase

            $mergeSourceCurrentCommit = Invoke-CustomCommand "git" @("rev-parse", "upstream-$branch") $trimmedWorkspace

            Write-Results $mergeSourceCurrentCommit

            $mergeResult = Invoke-CustomCommand "git" @("merge", "--no-commit", "--no-ff", "upstream-$branch") $trimmedWorkspace

            Write-Results $mergeResult

            if ($mergeBase.StdOut -eq $mergeSourceCurrentCommit.StdOut)
            {
                Write-Host "$space `"$name`" $url -"
            }
            elseif (-not $mergeResult.ExitCode -eq 0)
            {
                Write-Host "$space `"$name`" $url ×"
                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
            {
                Write-Host "$space `"$name`" $url >"
            }
        }
        else {
            Write-Verbose "`"$name`" is not a CaC project"
        }
    }
}

Provided under the Apache License version 2.0.

Report an issue

To use this template in Octopus Deploy, copy the JSON below and paste it into the Library → Step templates → Import dialog.

{
  "Id": "05210515-1a52-45d9-8be3-16caef808326",
  "Name": "Octopus - Find 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 determines if there are any changes to merge into the downstream project from the upstream project.\n\nThis indicates if changes to an upstream project are available to be merged into a downstream project, either automatically, or after resolving merge conflicts.",
  "Version": 4,
  "ExportedAt": "2023-11-16T20:09:13.659Z",
  "ActionType": "Octopus.AwsRunScript",
  "Author": "mcasperson",
  "Packages": [],
  "Parameters": [
    {
      "Id": "1eea75a0-74c7-4af9-8569-a9e9ece1bd55",
      "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": "1cf36824-079a-4cf1-b67c-36f07933f642",
      "Name": "FindConflicts.Git.Credentials.Password",
      "Label": "Git Password",
      "HelpText": "The git repo password or access token.",
      "DefaultValue": "",
      "DisplaySettings": {
        "Octopus.ControlType": "Sensitive"
      }
    },
    {
      "Id": "e6a7414d-8d84-4fb8-9149-2a87f20502bf",
      "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": "f5d0d829-ef6c-4e06-b115-7b2845339544",
      "Name": "FindConflicts.Git.Url.Host",
      "Label": "Git Hostname",
      "HelpText": "The git repo host name.",
      "DefaultValue": "github.com",
      "DisplaySettings": {
        "Octopus.ControlType": "SingleLineText"
      }
    },
    {
      "Id": "8b6e78e2-07fa-4783-9f9a-c23f5295f146",
      "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": "1e99c543-9e80-41b2-9384-8cbefd5c3ee6",
      "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": "b0131561-4928-4dfb-85a9-a1282ac4a1be",
      "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": "652fa73d-99fa-4c81-9ef8-2e79d985222d",
      "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": "7102a4ab-0f05-4b97-be26-8361b23df361",
      "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": "7015ee07-f265-41d4-863a-d1f13fcfbc68",
      "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    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    )\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    $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 Write-TerraformBackend {\n    Set-Content -Path 'backend.tf' -Value @\"\nterraform {\n        backend \"s3\" {}\n        required_providers {\n          octopusdeploy = { source = \"OctopusDeployLabs/octopusdeploy\", version = \"0.14.9\" }\n        }\n    }\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\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\n# Validate the inputs\nif ([string]::IsNullOrWhitespace($username)) {\n    Write-Error \"The FindConflicts.Git.Credentials.Username variable must be provided\"\n}\n\nif ([string]::IsNullOrWhitespace($password)) {\n    Write-Error \"The FindConflicts.Git.Credentials.Password variable must be provided\"\n}\n\nif ([string]::IsNullOrWhitespace($protocol)) {\n    Write-Error \"The FindConflicts.Git.Url.Protocol variable must be provided\"\n}\n\nif ([string]::IsNullOrWhitespace($gitHost)) {\n    Write-Error \"The FindConflicts.Git.Url.Host variable must be provided\"\n}\n\nif ([string]::IsNullOrWhitespace($org)) {\n    Write-Error \"The FindConflicts.Git.Url.Organization variable must be provided\"\n}\n\nif ([string]::IsNullOrWhitespace($repo)) {\n    Write-Error \"The FindConflicts.Git.Url.Template variable must be provided\"\n}\n\nif ([string]::IsNullOrWhitespace($region)) {\n    Write-Error \"The FindConflicts.Terraform.Backend.S3Region variable must be provided\"\n}\n\nif ([string]::IsNullOrWhitespace($key)) {\n    Write-Error \"The FindConflicts.Terraform.Backend.S3Key variable must be provided\"\n}\n\nif ([string]::IsNullOrWhitespace($bucket)) {\n    Write-Error \"The FindConflicts.Terraform.Backend.S3Bucket variable must be provided\"\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\n\nInvoke-CustomCommand \"git\" @(\"config\", \"--global\", \"user.email\", \"octopus@octopus.com\") | Write-Results\nInvoke-CustomCommand \"git\" @(\"config\", \"--global\", \"user.name\", \"Octopus Server\") | Write-Results\n\nInvoke-CustomCommand \"terraform\" @(\"init\", \"-no-color\", \"-backend-config=`\"bucket=$bucket`\"\", \"-backend-config=`\"region=$region`\"\", \"-backend-config=`\"key=$key`\"\") | Write-Results\n\nWrite-Host \"- Up to date\"\nWrite-Host \"> Can automatically merge\"\nWrite-Host \"× Merge conflict\"\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\n$downstreamCount = 0\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    $spaceId = Invoke-CustomCommand \"terraform\" @(\"output\", \"-raw\", \"octopus_space_id\")\n    $spaceName = Invoke-CustomCommand \"terraform\" @(\"output\", \"-raw\", \"octopus_space_name\")\n    $space = if ([string]::IsNullOrWhitespace($spaceName.StdOut))\n    {\n        $spaceId.StdOut | Format-StringAsNullOrTrimmed\n    }\n    else\n    {\n        $spaceName.StdOut | Format-StringAsNullOrTrimmed\n    }\n\n    foreach ($resource in $resources)\n    {\n        $url = $resource.values.git_library_persistence_settings.url | Format-StringAsNullOrTrimmed\n        $spaceId = $resource.values.space_id | Format-StringAsNullOrTrimmed\n        $name = $resource.values.name | Format-StringAsNullOrTrimmed\n\n        if (-not [string]::IsNullOrWhitespace($url))\n        {\n            $downstreamCount++\n\n            mkdir $trimmedWorkspace | Out-Null\n\n            Invoke-CustomCommand \"git\" @(\"clone\", $url, $trimmedWorkspace) | Write-Results\n            Invoke-CustomCommand \"git\" @(\"remote\", 'add', 'upstream', $templateRepo) $trimmedWorkspace | Write-Results\n            Invoke-CustomCommand \"git\" @(\"fetch\", \"--all\") $trimmedWorkspace | Write-Results\n            Invoke-CustomCommand \"git\" @(\"checkout\", \"-b\", \"upstream-$branch\", \"upstream/$branch\") $trimmedWorkspace | Write-Results\n\n            if (-not($branch -eq \"master\" -or $branch -eq \"main\"))\n            {\n                Invoke-CustomCommand \"git\" @(\"checkout\", \"-b\", $branch, \"origin/$branch\") $trimmedWorkspace | Write-Results\n            }\n            else\n            {\n                Invoke-CustomCommand \"git\" @(\"checkout\", $branch) $trimmedWorkspace | Write-Results\n            }\n\n            $mergeBase = Invoke-CustomCommand \"git\" @(\"merge-base\", $branch, \"upstream-$branch\") $trimmedWorkspace\n\n            Write-Results $mergeBase\n\n            $mergeSourceCurrentCommit = Invoke-CustomCommand \"git\" @(\"rev-parse\", \"upstream-$branch\") $trimmedWorkspace\n\n            Write-Results $mergeSourceCurrentCommit\n\n            $mergeResult = Invoke-CustomCommand \"git\" @(\"merge\", \"--no-commit\", \"--no-ff\", \"upstream-$branch\") $trimmedWorkspace\n\n            Write-Results $mergeResult\n\n            if ($mergeBase.StdOut -eq $mergeSourceCurrentCommit.StdOut)\n            {\n                Write-Host \"$space `\"$name`\" $url -\"\n            }\n            elseif (-not $mergeResult.ExitCode -eq 0)\n            {\n                Write-Host \"$space `\"$name`\" $url ×\"\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                Write-Host \"$space `\"$name`\" $url >\"\n            }\n        }\n        else {\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-find-cac-updates.json",
  "Website": "/step-templates/05210515-1a52-45d9-8be3-16caef808326",
  "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"
  }
}

History

Page updated on Thursday, November 16, 2023