-
Notifications
You must be signed in to change notification settings - Fork 8
Description
Context
The GitHubRepository class includes a CustomProperties property that represents the custom property values assigned to a repository. These are organization-defined key-value pairs used for categorization and automation (e.g., Type, SubscribeTo, Environment).
When working with repositories in scripts or automation, custom properties are frequently accessed to make decisions — for example, filtering repositories by type or checking which notification channels a repository subscribes to.
Request
The CustomProperties property on GitHubRepository is currently typed as [GitHubCustomProperty[]] — an array of objects, each with a Name and Value property. Accessing a specific custom property requires filtering the array:
# Current (cumbersome)
$repo.CustomProperties | Where-Object { $_.Name -eq 'Type' } | Select-Object -ExpandProperty ValueThe desired experience is to access custom property values directly as properties on an object:
# Desired (intuitive)
$repo.CustomProperties.Type
$repo.CustomProperties.SubscribeToThis makes the interface consistent with how PowerShell users expect to interact with structured data — using dot-notation on objects rather than filtering arrays of Name-Value pairs.
Multi-select values are flattened to a single string
GitHub custom properties support a multi_select type where the API returns an array of values. The current GitHubCustomProperty class types Value as [string], which causes PowerShell to join the array elements into a single space-delimited string:
# Current behavior — multi-select values are flattened
$repo.CustomProperties | Where-Object Name -eq SubscribeTo | Select-Object -ExpandProperty Value
# Returns: "CODEOWNERS Custom Instructions dependabot.yml gitattributes gitignore Hooks License Linter Settings Prompts PSModule Settings"
# This is a single string, not an array — iterating produces one item
$repo.CustomProperties | Where-Object Name -eq SubscribeTo |
Select-Object -ExpandProperty Value |
ForEach-Object { Write-Host "[$_]" }
# Output: [CODEOWNERS Custom Instructions dependabot.yml gitattributes gitignore Hooks License Linter Settings Prompts PSModule Settings]The expected behavior is that multi-select values are [string[]] arrays, so each value can be iterated individually:
# Desired behavior
$repo.CustomProperties.SubscribeTo
# Returns: @('Custom Instructions', 'License', 'Prompts')
$repo.CustomProperties.SubscribeTo | ForEach-Object { Write-Host "[$_]" }
# Output:
# [Custom Instructions]
# [License]
# [Prompts]Platform data types
The GitHub platform supports five custom property value types. The conversion must correctly handle all of them:
| Value type | API value_type |
API returns | Expected PowerShell type |
|---|---|---|---|
| Text string | string |
"some text" |
[string] |
| Single select | single_select |
"selected_value" |
[string] |
| Multi select | multi_select |
["val1", "val2"] |
[string[]] |
| True/false | true_false |
"true" or "false" |
[bool] |
| URL | url |
"https://..." |
[uri] |
Note
The values endpoint (/repos/{owner}/{repo}/properties/values) does not include the value_type in its response — only the property_name and value. The type distinction must be inferred from the value structure: multi_select values arrive as JSON arrays, true_false values arrive as the literal strings "true" or "false" (which must be converted to [bool]), and url values are detected as well-formed absolute URIs and converted to [uri]. All other scalar strings remain as [string].
Allowed characters and limits
Per the GitHub documentation:
- Property names:
a-z,A-Z,0-9,_,-,$,#only. No spaces. Maximum 75 characters. - Property values: All printable ASCII characters except
". Maximum 75 characters. - Allowed values (select types): Up to 200 values per property.
Test data
The PSModule/GitHub repository has the following custom properties configured, covering all five value types:
| Property | Value type | Value | Expected PowerShell result |
|---|---|---|---|
Archive |
true_false |
true |
[bool] $true |
Description |
string |
This is a test |
[string] 'This is a test' |
SubscribeTo |
multi_select |
Custom Instructions, License, Prompts |
[string[]] @('Custom Instructions', 'License', 'Prompts') |
Type |
single_select |
Module |
[string] 'Module' |
Upstream |
url |
https://github.com |
[uri] 'https://github.com' |
Acceptance criteria
$repo.CustomPropertiesis a[PSCustomObject]with dot-notation access$repo.CustomProperties.Typereturns'Module'as[string](single-select)$repo.CustomProperties.Descriptionreturns'This is a test'as[string](string)$repo.CustomProperties.SubscribeToreturns@('Custom Instructions', 'License', 'Prompts')as[string[]](multi-select)$repo.CustomProperties.Archivereturns$trueas[bool](true/false)$repo.CustomProperties.Upstreamreturns a[uri]with valuehttps://github.com(url)- Multi-select values are preserved as arrays, not flattened to space-delimited strings
- Properties with no value set return
$null - The change applies to both REST API and GraphQL code paths in the
GitHubRepositoryconstructor - All five platform data types (
string,single_select,multi_select,true_false,url) are correctly handled
Technical decisions
Type change for CustomProperties: Change the property type on GitHubRepository from [GitHubCustomProperty[]] to [PSCustomObject]. A PSCustomObject allows dynamic property names derived from the custom property names, enabling dot-notation access. This is the idiomatic PowerShell approach for dynamic key-value data.
Preserve value types from the API: The GitHub API returns multi-select custom property values as JSON arrays (e.g., ["CODEOWNERS", "Custom Instructions", "dependabot.yml"]). The current GitHubCustomProperty class declares [string] $Value, which causes PowerShell to coerce arrays to a single space-delimited string. With the new PSCustomObject approach, values must be stored with proper type handling — arrays stay as [string[]], scalar strings stay as [string], and null stays as $null.
true_false values are converted to [bool]: The GitHub API returns true_false custom property values as the literal strings "true" and "false". These are converted to [bool] ($true / $false) because boolean properties should be boolean in PowerShell. The conversion logic checks if the value equals "true" or "false" (case-insensitive) and converts accordingly. Since the values endpoint does not include value_type, this conversion is applied heuristically to values that are exactly "true" or "false". This is a safe heuristic because string and single_select properties that have "true" or "false" as a value would also benefit from boolean semantics, and the true_false type is the only type that constrains values to exactly these two strings.
url values are converted to [uri]: URL-type custom property values are converted to [uri] using [System.Uri]::new(). The detection heuristic uses [System.Uri]::IsWellFormedUriString($value, [System.UriKind]::Absolute) to identify well-formed absolute URIs before conversion. This correctly identifies values like https://github.com while leaving regular strings and single-select values untouched. The [uri] type is the idiomatic .NET/PowerShell representation for URLs and provides properties like Host, Scheme, AbsolutePath etc.
GitHubCustomProperty class: The existing GitHubCustomProperty class in
src/classes/public/Repositories/GitHubCustomProperty.ps1 is no longer needed for the repository property. It may still be useful for Get-GitHubRepositoryCustomProperty output, so evaluate whether to keep it or also update that function to return a PSCustomObject. Decide during implementation — if the class is not used elsewhere, remove it.
Construction approach (REST API path): In the REST constructor branch ($null -ne $Object.node_id), the custom_properties field from the GitHub REST API is already a flat object with property names as keys and values that may be strings or arrays. Convert it directly with type-appropriate handling:
$customProps = [PSCustomObject]@{}
if ($null -ne $Object.custom_properties) {
$Object.custom_properties.PSObject.Properties | ForEach-Object {
$value = if ($_.Value -is [System.Collections.IEnumerable] -and $_.Value -isnot [string]) {
[string[]]$_.Value
} elseif ($_.Value -is [string] -and ($_.Value -eq 'true' -or $_.Value -eq 'false')) {
$_.Value -eq 'true'
} elseif ($_.Value -is [string] -and [System.Uri]::IsWellFormedUriString($_.Value, [System.UriKind]::Absolute)) {
[uri]$_.Value
} else {
$_.Value
}
$customProps | Add-Member -NotePropertyName $_.Name -NotePropertyValue $value
}
}
$this.CustomProperties = $customPropsConstruction approach (GraphQL path): In the GraphQL constructor branch, the data comes as repositoryCustomPropertyValues.nodes — an array of objects with propertyName and value. The value field may be a string or an array. Convert it to a flat PSCustomObject with the same type handling:
$customProps = [PSCustomObject]@{}
if ($null -ne $Object.repositoryCustomPropertyValues -and $null -ne $Object.repositoryCustomPropertyValues.nodes) {
$Object.repositoryCustomPropertyValues.nodes | ForEach-Object {
$value = if ($_.value -is [System.Collections.IEnumerable] -and $_.value -isnot [string]) {
[string[]]$_.value
} elseif ($_.value -is [string] -and ($_.value -eq 'true' -or $_.value -eq 'false')) {
$_.value -eq 'true'
} elseif ($_.value -is [string] -and [System.Uri]::IsWellFormedUriString($_.value, [System.UriKind]::Absolute)) {
[uri]$_.value
} else {
$_.value
}
$customProps | Add-Member -NotePropertyName $_.propertyName -NotePropertyValue $value
}
}
$this.CustomProperties = $customPropsGet-GitHubRepositoryCustomProperty function: Update the output of this function to also return a
PSCustomObject for consistency. The REST endpoint /repos/{owner}/{repo}/properties/values returns an array of { property_name, value } objects — convert to a flat PSCustomObject before returning, applying the same type handling (arrays for multi-select, [bool] for true/false, [uri] for URLs, strings for the rest).
Breaking change: This is a breaking change to the CustomProperties interface. Code that currently
iterates $repo.CustomProperties expecting .Name and .Value properties will need to be updated. However, the new interface is significantly more intuitive and aligns with PowerShell conventions. Consider whether this warrants a Major label — given that custom properties are a relatively new feature and the old interface was not widely documented, treating this as Minor is reasonable.
Test approach: Tests use the PSModule/GitHub repository which has custom properties configured covering all five value types (see Test data section above). Tests are added to tests/Repositories.Tests.ps1 within the existing auth-case-based Context block. The tests retrieve the PSModule/GitHub repo and verify each custom property via dot-notation, checking both value and type. Tests run for all auth cases that have access to read the repository.
Implementation plan
Core changes
- Change the
CustomPropertiesproperty type inGitHubRepositoryclass from[GitHubCustomProperty[]]to[PSCustomObject]insrc/classes/public/Repositories/GitHubRepository.ps1 - Update the REST API constructor branch in
GitHubRepositoryto convertcustom_propertiesto aPSCustomObject, preserving array values for multi-select, converting"true"/"false"to[bool], and converting well-formed absolute URIs to[uri] - Update the GraphQL constructor branch in
GitHubRepositoryto convertrepositoryCustomPropertyValues.nodesto aPSCustomObject, preserving array values for multi-select, converting"true"/"false"to[bool], and converting well-formed absolute URIs to[uri] - Update
Get-GitHubRepositoryCustomPropertyinsrc/functions/public/Repositories/CustomProperties/Get-GitHubRepositoryCustomProperty.ps1to return aPSCustomObjectinstead of raw API response
Cleanup
- Evaluate whether
GitHubCustomPropertyclass insrc/classes/public/Repositories/GitHubCustomProperty.ps1is still needed; remove if unused - Update any internal code that references
CustomPropertiesas an array ofGitHubCustomPropertyobjects
Tests
Tests use the PSModule/GitHub repo with preconfigured custom properties covering all five data types.
- Add test verifying
CustomPropertiesis aPSCustomObject(not an array) onPSModule/GitHub - Add test for
Archive(true_false) —$repo.CustomProperties.Archiveis[bool]and equals$true - Add test for
Description(string) —$repo.CustomProperties.Descriptionis[string]and equals'This is a test' - Add test for
SubscribeTo(multi_select) —$repo.CustomProperties.SubscribeTois[string[]]and contains'Custom Instructions','License','Prompts' - Add test for
Type(single_select) —$repo.CustomProperties.Typeis[string]and equals'Module' - Add test for
Upstream(url) —$repo.CustomProperties.Upstreamis[uri]and equals'https://github.com' - Add test for
Get-GitHubRepositoryCustomPropertyreturning aPSCustomObjectwith the same values
Documentation
- Update function help for
Get-GitHubRepositoryCustomPropertywith new output format - Add usage example showing dot-notation access:
$repo.CustomProperties.Type - Add usage example showing multi-select iteration:
$repo.CustomProperties.SubscribeTo | ForEach-Object { ... }
Metadata
Metadata
Assignees
Labels
Type
Projects
Status