How can I add a file to an existing release from my build server?

My process is:

  1. Build Server: Build Artifacts
  2. Build Server: Publish Artifacts to Octopus
  3. Build Server: Create the release in Octopus and tell it to deploy release to Dev
  4. Octopus: Deploy to Dev
  5. Build Server: Assuming release to Dev is successful, promote to Test
  6. Octopus: Deploy to Test
  7. Build Server: Run integration tests on Test
  8. Upload results to Octopus?

It is step eight where I am falling apart, how can I upload the results to Octopus for that release?

The recommended approach here is to leverage the artifact feature in Octopus Deploy. One example of artifacts can be found in this database deployment. The artifact is associated with that deployment. In this example, a DBA can download the artifact, review it, then determine if the release should be promoted to the next environment.

Artifact generation was written with the assumption artifacts would be created as part of a deployment. The documentation reinforces that by only providing samples to upload via the helper method New-OctopusArtifact.

The good news is it is possible to upload an artifact to a release using the API. The tricky part is the endpoint expects a ServerTaskId, which is tied to a specific deployment. A deployment for a specific environment, and is also tied to a release, which is tied to a project, which is tied to a space.

To get that information the script will need to find (in order):

  1. The space id from the space name
  2. The project id from the project name
  3. The environment id from the environment name
  4. The release id using the project id and version number
  5. The most recent deployment for that release using the environment id

It looks like this (if written in PowerShell):

$header = @{ "X-Octopus-ApiKey" = $APIKey }

Write-Host "Getting the space information"
$spaceList = Invoke-RestMethod -Method Get -Uri "$OctopusUrl/api/spaces?skip=0&take=10&partialName=$([System.Web.HTTPUtility]::UrlEncode($spaceName))" -Headers $header
$space = $spaceList.Items | Where-Object {$_.Name -eq $spaceName}
$spaceId = $space.Id
Write-Host "The space-id for $spaceName is $spaceId"

Write-Host "Getting the environment information"
$environmentList = Invoke-RestMethod -Method Get -Uri "$OctopusUrl/api/$spaceId/environments?skip=0&take=10&partialName=$([System.Web.HTTPUtility]::UrlEncode($environmentName))" -Headers $header
$environment = $environmentList.Items | Where-Object {$_.Name -eq $environmentName}
$environmentId = $environment.Id
Write-Host "The id of $environmentName is $environmentId"

Write-Host "Getting the project information"
$projectList = Invoke-RestMethod -Method Get -Uri "$OctopusUrl/api/$spaceId/projects?skip=0&take=10&partialName=$([System.Web.HTTPUtility]::UrlEncode($projectName))" -Headers $header
$project = $projectList.Items | Where-Object {$_.Name -eq $projectName}
$projectId = $project.Id
Write-Host "The id of $projectName is $projectId"

Write-Host "Getting the release information"
$releaseList = Invoke-RestMethod -Method Get -Uri "$OctopusUrl/api/$spaceId/projects/$projectId/releases?skip=0&take=100&searchByVersion=$releaseVersion" -Headers $header
$release = $releaseList.Items | Where-Object {$_.Version -eq $releaseVersion}
$releaseId = $release.Id
Write-Host "The id of $releaseVersion is $releaseId"

Write-Host "Getting the deployment information"
$deploymentList = Invoke-RestMethod -Method Get -Uri "$OctopusUrl/api/$spaceId/releases/$releaseId/deployments?skip=0&take=1000" -Headers $header
$deploymentsToEnivronment = @($deploymentList.Items | Where-Object {$_.EnvironmentId -eq $environmentId})
$deploymentToUse = $null
$previousDate = Get-Date
$previousDate = $previousDate.AddDays(-10000)

foreach ($deployment in $deploymentsToEnivronment)
    if ($deployment.Created -gt $previousDate)
        $previousDate = $deployment.Created
        $deploymentToUse = $deployment

$serverTaskId = $deploymentToUse.TaskId
Write-Host "The server task id of the most recent deployment to $environmentName for release $releaseVersion is $serverTaskId"

The other tricky part is the artifact resource must exist before content can be uploaded to it. The order of operations is:

  1. Create artifact using the server task id
  2. Use ID returned from the POST in creating the artifact resource to upload the file content

Uploading file content in PowerShell is…quirky. This is how that looks all together:

Write-Host "Creating the artifact resource"
$artifact = @{  
  SpaceId = $spaceId
  Filename = $fileNameForOctopus
  Source = $null
  ServerTaskId = $serverTaskId  
  LogCorrelationId = $null
} | ConvertTo-Json -Depth 10

$uploadedArtifact = Invoke-RestMethod -Method Post -Uri "$OctopusUrl/api/$spaceId/artifacts" -Headers $header -Body $artifact
$artifactId = $uploadedArtifact.Id
Write-Host "Created the base artifact object with the id of $artifactId"

Write-Host "Preparing file upload"
$httpClientHandler = New-Object System.Net.Http.HttpClientHandler

$httpClient = New-Object System.Net.Http.HttpClient $httpClientHandler
$httpClient.DefaultRequestHeaders.Add("X-Octopus-ApiKey", $ApiKey)

$packageFileStream = New-Object System.IO.FileStream @($filePathToUpload, [System.IO.FileMode]::Open)

$contentDispositionHeaderValue = New-Object System.Net.Http.Headers.ContentDispositionHeaderValue "form-data"
$contentDispositionHeaderValue.Name = "fileData"
$contentDispositionHeaderValue.FileName = [System.IO.Path]::GetFileName($filePathToUpload)

$streamContent = New-Object System.Net.Http.StreamContent $packageFileStream
$streamContent.Headers.ContentDisposition = $contentDispositionHeaderValue
$ContentType = "multipart/form-data"
$streamContent.Headers.ContentType = New-Object System.Net.Http.Headers.MediaTypeHeaderValue $ContentType

$content = New-Object System.Net.Http.MultipartFormDataContent

$uploadUrl = "$OctopusUrl/api/$spaceId/artifacts/$artifactId/content"
Write-Host "Uploading file $filePathToUpload to $uploadUrl"
$httpClient.PutAsync($uploadUrl, $content).Result

Please see the full example script in the sample API GitHub repository.