Creating a GitHub Repository Tool - Part 3
We've come pretty far in building a PowerShell tool around a command-line utility. The fact that gh.exe
can output JSON makes all the difference in the world. Without, I would have to parse text output which is much more error-prone and tedious. But with JSON, I can use ConvertFrom-Json
to convert the JSON output into a PowerShell object. This makes it much easier to work with the data and turn it into rich PowerShell objects.
In the last part, I demonstrated how I used a PowerShell class to represent my GitHub repository information. Using the class also simplifies the code that uses it, although I did create a helper function to create a new instance of the class. Now, I can easily get a structured object that represents a GitHub repository, at least the parts I care about.
PS C:\> Get-Githubrepo ADReportingTools
Name : ADReportingTools
Description : 🧰 A set of PowerShell commands to gather information and
create reports from Active Directory. 👨 👩 💻 This
project relies on the Active Directory module from
Microsoft.
LastUpdate : 10/8/2024 10:26:48 AM
Visibility : Public
DefaultBranch : main
LatestReleaseName : ADReportingTools_v1.4.0
LatestReleaseDate : 7/18/2022 10:27:54 AM
LastPush : 3/3/2024 3:21:38 PM
StargazerCount : 97
WatcherCount : 5
URL : https://github.com/jdhitsolutions/ADReportingTools
DiskUsage : 11066
ID : MDEwOlJlcG9zaXRvcnkzMzk0Mzk0Njc=
By the way, I'm capturing the repository ID because I am assuming it is a unique identifier and there might be a need to use it in the future. I'm also capturing the disk usage because I might want to use that in the future as well. I'm not sure what I might do with these properties, but it seems like useful information to have. I think it is better to have too much information than not enough. As you'll see, when I get to formatting, I can easily hide these properties if I don't want to display them by default. But they are there if I need them.
Looking at the object, I can verify that I have a structured object with a defined typename and property cast properties.
PS C:\> Get-Githubrepo ADReportingTools | Get-Member
TypeName: GitHubRepoInfo
Name MemberType Definition
---- ---------- ----------
Equals Method bool Equals(System.Object obj)
GetHashCode Method int GetHashCode()
GetType Method type GetType()
ToString Method string ToString()
DefaultBranch Property string DefaultBranch {get;set;}
Description Property string Description {get;set;}
DiskUsage Property int DiskUsage {get;set;}
ID Property string ID {get;set;}
LastPush Property datetime LastPush {get;set;}
LastUpdate Property datetime LastUpdate {get;set;}
LatestReleaseDate Property datetime LatestReleaseDate {get;set;}
LatestReleaseName Property string LatestReleaseName {get;set;}
Name Property string Name {get;set;}
StargazerCount Property int StargazerCount {get;set;}
URL Property string URL {get;set;}
Visibility Property ghRepoVisibility Visibility {get;set;}
WatcherCount Property int WatcherCount {get;set;}
These are the properties I'm pulling from GitHub.
Type Extensions
However, I don't have to stop there. I can do a lot of reporting and analysis with the object as it currently stands. But I can also extend the object with additional properties. For example, I might want to add a property that calculates the number of days since the last update. I can do this by creating a type extension.
Update-TypeData -TypeName GitHubRepoInfo -MemberType ScriptProperty -MemberName UpdateAge -Value {(Get-Date) - $this.LastUpdate} -force
This is why I wanted an object with a defined type name. Here are a few other type extensions I added
#Add a boolean property to indicate if the repository has a release
Update-TypeData -TypeName GitHubRepoInfo -MemberType ScriptProperty -MemberName isReleased -Value {$this.LatestReleaseName -match "\w+" } -force
#Add a ReleaseAge script property
Update-TypeData -TypeName GitHubRepoInfo -MemberType ScriptProperty -MemberName ReleaseAge -Value {(Get-Date) - $this.LatestReleaseDate } -force
#If there is no default branch, assume the repository IsEmpty
Update-TypeData -TypeName GitHubRepoInfo -MemberType ScriptProperty -MemberName IsEmpty -Value {-Not $this.DefaultBranch} -force
These properties will be available on any instance of the class. Even on imported data.
With this information, I can either delete the repository or take some other action. Here you can see the new properties.
PS C:\> $in | Get-Random
Name : MyTimer
Description : DEPRECATED A PowerShell module that contains a collection
of simple functions that you can use to create and manage
timers.
LastUpdate : 9/25/2018 11:42:15 AM
Visibility : Private
DefaultBranch : master
LatestReleaseName :
LatestReleaseDate : 1/1/0001 12:00:00 AM
LastPush : 7/17/2018 10:38:52 AM
StargazerCount : 0
WatcherCount : 1
URL : https://github.com/jdhitsolutions/MyTimer
DiskUsage : 23
ID : MDEwOlJlcG9zaXRvcnk2Mjg5NjMyNg==
UpdateAge : 2206.23:29:24.5892098 #<---
isReleased : False #<---
ReleaseAge : 739168.11:11:39.5893143 #<---
IsEmpty : False #<---
As I look at this, I realize there are a few more additions I could make.
Adding a Custom Method
My likely use case is that I will work with a variable containing a collection of these objects. There might be times when I want to refresh the object to get the latest information. The script properties will always be up to date every time I access the object. I need a method to refresh the object.
If you recall, I didn't define any methods in the class. I could revisit the class and define a method. However, you can't write a Pester test for a class method. I could write the method to call external functions that I could test. But it is just as easy to add a method to the object using a type extension.
Update-TypeData -TypeName GitHubRepoInfo -MemberType ScriptMethod -MemberName Refresh -Value {
$refresh = Get-GitHubRepo -Repository $this.Name
#get type extension members
$ex = (Get-TypeData GitHubRepoInfo).members.keys
#Get standard properties and update the values from the refreshed object
$this.PSObject.Properties.Where({$ex -NotContains $_.Name}) | ForEach-Object {
$_.Value = $refresh.$($_.Name)
}
} -force
There's no reason to re-invent the wheel. I can use my existing function to get the latest information. The tricky part is updating the values on the original object from the refreshed object. I'm filtering out the properties defined by type extensions because they will always be up to date. I only need to update the standard properties.
In looking at my output, there is one more piece of information I should add. When I look at an instance of the GitHubRepoInfo
object, I have no way of knowing when I captured the information. This will be even more relevant when working with exported and imported data. The question is whether I should capture this information when I get the raw JSON data from GitHub or when I create the object. I'm going to need a DateTime property like InfoDate
but where's the best place to put it?
If I include it in code that gets the raw JSON data, I'm going to have to add it as an additional JSON entry. I would then need to revise my code that creates an instance of my GitHubRepoInfo
class to use this value.
I think I am going to have to deal with this in several places because there are several use cases.
- I import previously exported raw JSON data and create objects
- I run
Get-GitHubRepo
to get a repository directly from GitHub