Find unused projects

This script will search for projects who haven’t had a release created in the previous set number of days.

Please note, this script will exclude projects:

  • Without any releases.
  • Projects already disabled.

Usage

Provide values for the following:

  • Octopus URL
  • Octopus API Key
  • Disable Old Projects - indicates if the projects should be set to disabled, default is $false
  • Days Since Last Release - the number of days to allow before considering the project is inactive, default is 90

Script

PowerShell (REST API)
[Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12

$octopusUrl = "https://your-octopus-url" ## Octopus URL to look at
$octopusApiKey = "API-YOUR-KEY" ## API key of user who has permissions to view all spaces, cancel tasks, and resubmit runbooks runs and deployments
$disableOldProjects = $false ## Tells the script to disable the projects that are older than the days since last release
$daysSinceLastRelease = 90 ## The number of days since the last release to be considered unused.  Any project without a release created in [90] days is considered inactive.

$cachedResults = @{}

function Invoke-OctopusApi
{
    param
    (
        $octopusUrl,
        $endPoint,
        $spaceId,
        $apiKey,
        $method,
        $item,
        $ignoreCache     
    )

    $octopusUrlToUse = $OctopusUrl
    if ($OctopusUrl.EndsWith("/"))
    {
        $octopusUrlToUse = $OctopusUrl.Substring(0, $OctopusUrl.Length - 1)
    }

    if ([string]::IsNullOrWhiteSpace($SpaceId))
    {
        $url = "$octopusUrlToUse/api/$EndPoint"
    }
    else
    {
        $url = "$octopusUrlToUse/api/$spaceId/$EndPoint"    
    }  

    try
    {        
        if ($null -ne $item)
        {
            $body = $item | ConvertTo-Json -Depth 10
            Write-Verbose $body

            Write-Host "Invoking $method $url"
            return Invoke-RestMethod -Method $method -Uri $url -Headers @{"X-Octopus-ApiKey" = "$ApiKey" } -Body $body -ContentType 'application/json; charset=utf-8' 
        }

        if (($null -eq $ignoreCache -or $ignoreCache -eq $false) -and $method.ToUpper().Trim() -eq "GET")
        {
            Write-Verbose "Checking to see if $url is already in the cache"
            if ($cachedResults.ContainsKey($url) -eq $true)
            {
                Write-Verbose "$url is already in the cache, returning the result"
                return $cachedResults[$url]
            }
        }
        else
        {
            Write-Verbose "Ignoring cache."    
        }

        Write-Verbose "No data to post or put, calling bog standard Invoke-RestMethod for $url"
        $result = Invoke-RestMethod -Method $method -Uri $url -Headers @{"X-Octopus-ApiKey" = "$ApiKey" } -ContentType 'application/json; charset=utf-8'

        if ($cachedResults.ContainsKey($url) -eq $true)
        {
            $cachedResults.Remove($url)
        }
        Write-Verbose "Adding $url to the cache"
        $cachedResults.add($url, $result)

        return $result

               
    }
    catch
    {
        if ($null -ne $_.Exception.Response)
        {
            if ($_.Exception.Response.StatusCode -eq 401)
            {
                Write-Error "Unauthorized error returned from $url, please verify API key and try again"
            }
            elseif ($_.Exception.Response.statusCode -eq 403)
            {
                Write-Error "Forbidden error returned from $url, please verify API key and try again"
            }
            else
            {                
                Write-Verbose -Message "Error calling $url $($_.Exception.Message) StatusCode: $($_.Exception.Response.StatusCode )"
            }            
        }
        else
        {
            Write-Verbose $_.Exception
        }
    }

    Throw $_.Exception
}

$spaceList = Invoke-OctopusApi -octopusUrl $octopusUrl -apiKey $octopusApiKey -endPoint "spaces?skip=0&take=1000" -spaceId $null -method "GET"

$currentUtcTime = $(Get-Date).ToUniversalTime()

$oldProjectList = @()
foreach ($space in $spaceList.Items)
{    
    $projectList = Invoke-OctopusApi -octopusUrl $octopusUrl -apiKey $octopusApiKey -endPoint "projects?skip=0&take=10000" -spaceId $space.Id -method "GET"    

    foreach ($project in $projectList.Items)
    {
        if ($project.IsDisabled -eq $true)
        {
            Write-Verbose "Project $($project.Name) is already disabled."
            continue
        }

        $releaseList = Invoke-OctopusApi -octopusUrl $octopusUrl -apiKey $octopusApiKey -endPoint "projects/$($project.Id)/releases" -spaceId $space.Id -method "GET"

        if ($releaseList.Items.Count -le 0)
        {
            Write-Verbose "No releases found for $($project.Name)."
            continue
        }

        $assembledDate = [datetime]::Parse($releaseList.Items[0].Assembled)
        $assembledDate = $assembledDate.ToUniversalTime()

        $dateDiff = $currentUtcTime - $assembledDate

        if ($dateDiff.TotalDays -gt $daysSinceLastRelease)
        {
            $oldProjectList += "$($project.Name) - $($space.Name) last release was $($dateDiff.TotalDays) days ago."

            if ($disableOldProjects -eq $true)
            {
                $project.IsDisabled = $true
                $updatedProject = Invoke-OctopusApi -octopusUrl $octopusUrl -apiKey $octopusApiKey -endPoint "projects/$($project.Id)" -spaceId $space.Id -method "PUT" -Item $project
                Write-Host "Set the project $($updatedProject.Name) to disabled."
            }
        }        
    }
}

Write-Host "The following projects were found to have no releases created in at least $daysSinceLastRelease days."
foreach ($project in $oldProjectList)
{
    Write-Host "    $project"
}
PowerShell (Octopus.Client)
# Load assembly
Add-Type -Path 'path:\to\Octopus.Client.dll'
$octopusURL = "https://your-octopus-url"
$octopusAPIKey = "API-YOUR-KEY"
$daysSinceLastRelease = 90

$endpoint = New-Object Octopus.Client.OctopusServerEndpoint($octopusURL, $octopusAPIKey)
$repository = New-Object Octopus.Client.OctopusRepository($endpoint)
$client = New-Object Octopus.Client.OctopusClient($endpoint)

$currentUtcTime = $(Get-Date).ToUniversalTime()
$oldProjects = @()

# Loop through spaces
foreach ($space in $repository.Spaces.GetAll())
{
    # Get space
    $space = $repository.Spaces.FindByName($space.Name)
    $repositoryForSpace = $client.ForSpace($space)

    # Get all projects in space
    $projects = $repositoryForSpace.Projects.GetAll()

    # Loop through projects
    foreach ($project in $projects)
    {
        # Check for disabled
        if ($project.IsDisabled)
        {
            Write-Host "$($project.Name) is disabled."
            continue
        }

        # Get project releases
        $releases = $repositoryForSpace.Projects.GetReleases($project)

        if ($releases.Items.Count -eq 0)
        {
            Write-Host "No releases found for $($project.Name)"
            continue
        }

        $assembledDate = [datetime]::Parse($releases.Items[0].Assembled)
        $assembledDate = $assembledDate.ToUniversalTime()
        
        $dateDiff = $currentUtcTime - $assembledDate

        # Check the length of time
        if ($dateDiff.TotalDays -gt $daysSinceLastRelease)
        {
            $oldProjects += "$($project.Name) - $($space.Name) last release was $($dateDiff.TotalDays) days ago."

        }
    }
}


Write-Host "The following projects were found to have no releases created in at least $daysSinceLastRelease days"
foreach ($project in $oldProjects)
{
    Write-Host "`t$project"
}
C#
// If using .net Core, be sure to add the NuGet package of System.Security.Permissions
#r "path\to\Octopus.Client.dll"

using Octopus.Client;
using Octopus.Client.Model;

var octopusURL = "https://your-octopus-url";
var octopusAPIKey = "API-YOUR-KEY";
DateTime currentUtcTime = DateTime.Now.ToUniversalTime();
System.Collections.Generic.List<string> oldProjects = new System.Collections.Generic.List<string>();
int daysSinceLastRelease = 90;

// Create repository object
var endpoint = new OctopusServerEndpoint(octopusURL, octopusAPIKey);
var repository = new OctopusRepository(endpoint);
var client = new OctopusClient(endpoint);

// Loop through all spaces
foreach (var octopusSpace in repository.Spaces.FindAll())
{
    // Get space repository
    var space = repository.Spaces.FindByName(octopusSpace.Name);
    var repositoryForSpace = client.ForSpace(space);

    // Get all projects
    var projects = repositoryForSpace.Projects.GetAll();

    // Loop through projects
    foreach (var project in projects)
    {
        if(project.IsDisabled)
        {
            Console.WriteLine(string.Format("{0} is disabled", project.Name));
            continue;
        }

        // Get releases for project
        var releases = repositoryForSpace.Projects.GetAllReleases(project);

        // Check to see if anything has ever been created
        if (releases.Count == 0)
        {
            Console.WriteLine(string.Format("No releases found for {0}", project.Name));
            continue;
        }

        var assembledDate = releases[0].Assembled.ToUniversalTime();
        var dateDiff = currentUtcTime - assembledDate;

        // Check to see how many days it has been 
        if (dateDiff.TotalDays > daysSinceLastRelease)
        {
            oldProjects.Add(string.Format("{0} - {1} last release was {2} days ago.", project.Name, space.Name, dateDiff.TotalDays.ToString()));
        }
    }
}

Console.WriteLine(string.Format("The following projects were found to have no releases created in at least {0} days", daysSinceLastRelease));
foreach(var project in oldProjects)
{
    Console.WriteLine(string.Format("\t {0}", project));
}
Python3
import json
import requests
from requests.api import get, head
import datetime
from dateutil.parser import parse

def get_octopus_resource(uri, headers, skip_count = 0):
    items = []
    skip_querystring = ""

    if '?' in uri:
        skip_querystring = '&skip='
    else:
        skip_querystring = '?skip='

    response = requests.get((uri + skip_querystring + str(skip_count)), headers=headers)
    response.raise_for_status()

    # Get results of API call
    results = json.loads(response.content.decode('utf-8'))

    # Store results
    if hasattr(results, 'keys') and 'Items' in results.keys():
        items += results['Items']

        # Check to see if there are more results
        if (len(results['Items']) > 0) and (len(results['Items']) == results['ItemsPerPage']):
            skip_count += results['ItemsPerPage']
            items += get_octopus_resource(uri, headers, skip_count)

    else:
        return results

    
    # return results
    return items

octopus_server_uri = 'https://your-octopus-url'
octopus_api_key = 'API-YOUR-KEY'
headers = {'X-Octopus-ApiKey': octopus_api_key}
old_projects = []
current_date = datetime.datetime.utcnow()
days_since_last_release = 90

# Get spaces
uri = '{0}/api/spaces'.format(octopus_server_uri)
spaces = get_octopus_resource(uri, headers)

# Loop through spaces
for space in spaces:
    # Get all projects
    uri = '{0}/api/{1}/projects'.format(octopus_server_uri, space['Id'])
    projects = get_octopus_resource(uri, headers)

    # Loop through projects
    for project in projects:
        
        # Check to see if it's disabled
        if project['IsDisabled']:
            print('{0} is disabled', project['Name'])
            continue
        
        # Get releases
        uri = '{0}/api/{1}/projects/{2}/releases'.format(octopus_server_uri, space['Id'], project['Id'])
        releases = get_octopus_resource(uri, headers)

        # Check to see if any exist
        if len(releases) == 0:
            print('No releases found for {0}'.format(project['Name']))
            continue

        # Get the assembled date
        assembled_date = parse(releases[0]['Assembled'])
        assembled_date = assembled_date.replace(tzinfo=None)

        # Calculate the difference
        date_diff = current_date - assembled_date
        
        if date_diff.days > days_since_last_release:
            old_projects.append('{0} - {1} last release as {2} days ago'.format(project['Name'], space['Name'], date_diff.days))

print('The following projects were found to have no releases created in the last {0} days'.format(days_since_last_release))

for project in old_projects:
    print('\t{0}'.format(project))
Go
package main

import (
	"fmt"
	"log"
	"net/url"
	"time"

	"github.com/OctopusDeploy/go-octopusdeploy/octopusdeploy"
)

func main() {

	apiURL, err := url.Parse("https://your-octopus-url")
	if err != nil {
		log.Println(err)
	}
	APIKey := "API-YOUR-KEY"

	// Get client
	client := octopusAuth(apiURL, APIKey, "")

	// Get current date
	currentDate := time.Now()
	daysSinceLastRelease := 90
	oldProjects := []string{}

	// Get all spaces
	spaces, err := client.Spaces.GetAll()

	if err != nil {
		log.Println(err)
	}

	// Loop through spaces
	for _, space := range spaces {
		spaceClient := octopusAuth(apiURL, APIKey, space.ID)

		// Get all projects for space
		projects, err := spaceClient.Projects.GetAll()

		if err != nil {
			log.Println(err)
		}

		// Loop through projects in space
		for _, project := range projects {
			// Check to see if it is disabled
			if project.IsDisabled {
				fmt.Printf("%[1]s is disabled \n", project.Name)
				continue
			}

			// Get all releases
			projectReleases, err := spaceClient.Projects.GetReleases(project)

			if err != nil {
				log.Println(err)
			}

			if len(projectReleases) == 0 {
				fmt.Printf("No releases found for %[1]s \n", project.Name)
				continue
			}

			// Get assembled date of most recent release
			assembledDate := projectReleases[0].Assembled

			// Calculate difference
			dateDiff := currentDate.Sub(assembledDate).Hours() / 24
			
			strDateDiff := fmt.Sprintf("%f", dateDiff)

			// Check the difference
			if dateDiff > float64(daysSinceLastRelease) {
				oldProjects = append(oldProjects, (project.Name + " - " + space.Name + " last release was " + strDateDiff + " days ago."))
			}
		}
	}

	strDaysSinceLastRelease := fmt.Sprintf("%f", daysSinceLastRelease)
	fmt.Printf("The following projects were found to have no releases created in at least %[1]s days \n", strDaysSinceLastRelease)
	for _, project := range oldProjects {
		fmt.Println("\t" + project)
	}
}

func octopusAuth(octopusURL *url.URL, APIKey, space string) *octopusdeploy.Client {
	client, err := octopusdeploy.NewClient(nil, octopusURL, APIKey, space)
	if err != nil {
		log.Println(err)
	}

	return client
}

func GetSpace(octopusURL *url.URL, APIKey string, spaceName string) *octopusdeploy.Space {
	client := octopusAuth(octopusURL, APIKey, "")

	spaceQuery := octopusdeploy.SpacesQuery{
		Name: spaceName,
	}

	// Get specific space object
	spaces, err := client.Spaces.Get(spaceQuery)

	if err != nil {
		log.Println(err)
	}

	for _, space := range spaces.Items {
		if space.Name == spaceName {
			return space
		}
	}

	return nil
}

func GetUserRole(client *octopusdeploy.Client, userRoleName string) *octopusdeploy.UserRole {
	// Get all roles
	userRoles, err := client.UserRoles.GetAll()

	if err != nil {
		log.Println(err)
	}

	for _, userRole := range userRoles {
		if userRole.Name == userRoleName {
			return userRole
		}
	}

	return nil
}

Help us continuously improve

Please let us know if you have any feedback about this page.

Send feedback

Page updated on Sunday, January 1, 2023