Last active
September 25, 2025 07:44
-
-
Save heaths/c23b45206ca08b2293cba191288b34cb to your computer and use it in GitHub Desktop.
Add or remove crate owners
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env pwsh | |
#Requires -Version 7 | |
[CmdletBinding(SupportsShouldProcess)] | |
param ( | |
[Parameter(Mandatory)] | |
[securestring] $Token, | |
[Parameter(Mandatory)] | |
[string] $Owner, | |
[Parameter()] | |
[string[]] $Add, | |
[Parameter()] | |
[string[]] $Remove, | |
[Parameter()] | |
[ValidateNotNullOrWhiteSpace()] | |
[string] $Filter, | |
[Parameter()] | |
[switch] $Force, | |
[Parameter()] | |
[switch] $PassThru | |
) | |
$ErrorActionPreference = 'Stop' | |
$headers = @{ | |
Authorization = 'Bearer ' + (ConvertFrom-SecureString -SecureString $Token -AsPlainText) | |
'Content-Type' = 'application/json' | |
} | |
# Get all crates owned by $Owner | |
$ownerId = (Invoke-WebRequest "https://crates.io/api/v1/users/$Owner" -Verbose:$false | ConvertFrom-Json).user.id | |
Write-Verbose "Getting all crates owned by $Owner ($ownerId)" | |
$next_page = "?user_id=$ownerId&per_page=100" | |
$crates = while ($next_page) { | |
$resp = Invoke-WebRequest "https://crates.io/api/v1/crates$next_page" -Verbose:$false | |
$json = $resp.Content | ConvertFrom-Json | |
$next_page = $json.meta.next_page | |
$json.crates | Where-Object name -Match $Filter | |
} | |
Write-Verbose "Found $($crates.Length) crates owned by $Owner ($ownerId)" | |
# Given rate limit of 1 authenticated request/second (https://crates.io/data-access#api), | |
# we'll determine what changes we need to make, if any, before attempting to update owners. | |
for ($i = 0; $i -lt $crates.Length; $i++) { | |
$crate = $crates[$i] | |
if ($PassThru) { | |
$crate | |
} | |
$crateName = $crate.name | |
$progress = @{ | |
Activity = 'Updating owners' | |
Status = "Updating $crateName" | |
PercentComplete = $i * 100 / $crates.Length | |
} | |
Write-Progress @progress -CurrentOperation "Getting owners of $crateName" | |
$owners = (Invoke-WebRequest "https://crates.io/api/v1/crates/$crateName/owners" -Verbose:$false | ConvertFrom-Json).users | |
[string[]] $addOwners = foreach ($addOwner in $Add) { | |
if ($owners.login -notcontains $addOwner) { | |
$addOwner | |
} | |
} | |
[string[]] $removeOwners = foreach ($removeOwner in $Remove) { | |
if ($owners.login -contains $removeOwner) { | |
$removeOwner | |
} | |
} | |
if ($addOwners.Length -gt 0) { | |
$addOwnersString = $addOwners -join ',' | |
Write-Progress @progress -CurrentOperation "Adding owners $addOwnersString to $crateName" | |
if ($Force -or $PSCmdlet.ShouldProcess("Add $addOwnersString to $crateName", "Add $addOwnersString to $crateName?", "Add owners")) { | |
$json = @{users=$addOwners} | ConvertTo-Json | |
Invoke-RestMethod -Headers $headers -Method Put -Uri "https://crates.io/api/v1/crates/$crateName/owners" -Body $json -ErrorAction SilentlyContinue -ErrorVariable err | Out-Null | |
if ($err) { | |
Write-Warning "Failed to add owners to ${crateName}:`n$($err.Exception.Message)" | |
} | |
Start-Sleep -Seconds 1 | |
} | |
} | |
if ($removeOwners.Length -gt 0) { | |
$removeOwnersString = $addOwners -join ',' | |
Write-Progress @progress -CurrentOperation "Removing owners $removeOwnersString from $crateName" | |
if ($Force -or $PSCmdlet.ShouldProcess("Remove $removeOwnersString from $crateName", "Remove $removeOwnersString from $crateName?", "Remove owners")) { | |
$json = @{users=$removeOwners} | ConvertTo-Json | |
Invoke-RestMethod -Headers $headers -Method Delete -Uri "https://crates.io/api/v1/crates/$crateName/owners" -Body $json -ErrorAction SilentlyContinue -ErrorVariable err | Out-Null | |
if ($err) { | |
Write-Warning "Failed to remove owners from ${crateName}:`n$($err.Exception.Message)" | |
} | |
Start-Sleep -Seconds 1 | |
} | |
} | |
} | |
<# | |
.SYNOPSIS | |
Add or remove crate owners. | |
.DESCRIPTION | |
When run by the crate owner passed as `-Owner`, you can add and/or remove crate owners. | |
This can be slow to run since there is a limit of 1 authenticated request/second, | |
but the script will check that it needs to do anything for each crate to help mitigate that | |
as well as sleep in between authenticated modification requests. | |
.PARAMETER Token | |
A token with `change-owners` permissions from <https://crates.io/settings/tokens>. | |
Use `$token = Read-Host -AsSecureString` to paste the token into PowerShell. | |
.PARAMETER Owner | |
The crates' owner login. | |
.PARAMETER Add | |
A list of owner logins to add. | |
.PARAMETER Remove | |
A list of owner logins to remove. | |
.PARAMETER Filter | |
A regular expression compatible with `-Match` to select which owned crates to update. | |
.PARAMETER Force | |
Do not prompt to update owners. | |
.PARAMETER PassThru | |
Write any found crates to output regardless of whether they were updated. | |
.EXAMPLE | |
PS> ./Edit-CrateOwners.ps1 -Token (Read-Host -AsSecureString) -Owner heaths -Add microsoft-oss-releases | |
Add "microsoft-oss-releases" to the owners for any crates already owned by "heaths". | |
.EXAMPLE | |
PS> ./Edit-CrateOwners.ps1 -Token (Read-Host -AsSecureString) -Owner heaths -Remove microsoft-oss-releases -Filter '^(?!(azure|typespec))` | |
Remove "microsoft-oss-releases" from the owners for any crates owned by "heaths" that do not start with "azure" or "typespec". | |
.EXAMPLE | |
PS> ./Edit-CrateOwners.ps1 -Token (Read-Host -AsSecureString) -Owner heaths -Add microsoft-oss-releases -WhatIf | |
Check to which crates "microsoft-oss-releases" will be added as an owner without actually updating the crates. | |
#> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment