Skip to content

Instantly share code, notes, and snippets.

@heaths
Last active September 25, 2025 07:44
Show Gist options
  • Save heaths/c23b45206ca08b2293cba191288b34cb to your computer and use it in GitHub Desktop.
Save heaths/c23b45206ca08b2293cba191288b34cb to your computer and use it in GitHub Desktop.
Add or remove crate owners
#!/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