go-git-tools/cmd/git-package-cleanup/main.go
2025-09-22 15:21:35 +02:00

212 lines
4.9 KiB
Go

package main
import (
"bufio"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"strings"
)
type PackageVersion struct {
ID int `json:"id"`
Name string `json:"name"`
}
type GitHubClient struct {
token string
owner string
client *http.Client
baseURL string
}
func NewGitHubClient(token, owner string) *GitHubClient {
return &GitHubClient{
token: token,
owner: owner,
client: &http.Client{},
baseURL: "https://api.github.com",
}
}
func (g *GitHubClient) makeRequest(method, url string) (*http.Response, error) {
req, err := http.NewRequest(method, url, nil)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+g.token)
req.Header.Set("Accept", "application/vnd.github+json")
req.Header.Set("X-GitHub-Api-Version", "2022-11-28")
return g.client.Do(req)
}
func (g *GitHubClient) getPackageVersions(packageName, packageType string) ([]PackageVersion, error) {
url := fmt.Sprintf("%s/users/%s/packages/%s/%s/versions", g.baseURL, g.owner, packageType, packageName)
var allVersions []PackageVersion
page := 1
for {
pageURL := fmt.Sprintf("%s?page=%d&per_page=100", url, page)
resp, err := g.makeRequest("GET", pageURL)
if err != nil {
return nil, fmt.Errorf("request failed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("API error %d: %s", resp.StatusCode, string(body))
}
var versions []PackageVersion
if err := json.NewDecoder(resp.Body).Decode(&versions); err != nil {
return nil, fmt.Errorf("decode failed: %w", err)
}
if len(versions) == 0 {
break
}
allVersions = append(allVersions, versions...)
page++
}
return allVersions, nil
}
func (g *GitHubClient) deleteVersion(packageName, packageType string, versionID int) error {
url := fmt.Sprintf("%s/users/%s/packages/%s/%s/versions/%d",
g.baseURL, g.owner, packageType, packageName, versionID)
resp, err := g.makeRequest("DELETE", url)
if err != nil {
return fmt.Errorf("request failed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusNoContent {
body, _ := io.ReadAll(resp.Body)
return fmt.Errorf("delete failed %d: %s", resp.StatusCode, string(body))
}
return nil
}
func findVersionsToDelete(versions []PackageVersion, keepVersion string) []PackageVersion {
var toDelete []PackageVersion
found := false
// Find the version to keep and mark everything after it for deletion
for i := len(versions) - 1; i >= 0; i-- {
if versions[i].Name == keepVersion {
found = true
break
}
toDelete = append([]PackageVersion{versions[i]}, toDelete...)
}
if !found {
fmt.Printf("Warning: Version '%s' not found in package versions\n", keepVersion)
return nil
}
return toDelete
}
func getUserInput(prompt string) string {
fmt.Print(prompt)
scanner := bufio.NewScanner(os.Stdin)
scanner.Scan()
return strings.TrimSpace(scanner.Text())
}
func confirmDeletion(count int) bool {
response := getUserInput(fmt.Sprintf("Delete %d versions? (y/N): ", count))
return strings.ToLower(response) == "y"
}
func main() {
token := os.Getenv("GITHUB_TOKEN")
if token == "" {
fmt.Println("Error: GITHUB_TOKEN environment variable is required")
fmt.Println("Create a token with 'packages:read' and 'packages:delete' scopes")
os.Exit(1)
}
owner := getUserInput("Owner (username/org): ")
if owner == "" {
fmt.Println("Error: Owner is required")
os.Exit(1)
}
packageName := getUserInput("Package name: ")
if packageName == "" {
fmt.Println("Error: Package name is required")
os.Exit(1)
}
packageType := getUserInput("Package type [npm]: ")
if packageType == "" {
packageType = "npm"
}
client := NewGitHubClient(token, owner)
fmt.Printf("Fetching versions for %s/%s...\n", owner, packageName)
versions, err := client.getPackageVersions(packageName, packageType)
if err != nil {
fmt.Printf("Error fetching versions: %v\n", err)
os.Exit(1)
}
if len(versions) == 0 {
fmt.Println("No versions found")
return
}
fmt.Printf("\nFound %d versions:\n", len(versions))
for i, v := range versions {
fmt.Printf("%d. %s (ID: %d)\n", i+1, v.Name, v.ID)
}
keepVersion := getUserInput("\nVersion to keep (and newer): ")
if keepVersion == "" {
fmt.Println("Error: Version is required")
os.Exit(1)
}
toDelete := findVersionsToDelete(versions, keepVersion)
if len(toDelete) == 0 {
fmt.Println("No versions to delete")
return
}
fmt.Printf("\nVersions to delete:\n")
for _, v := range toDelete {
fmt.Printf("- %s (ID: %d)\n", v.Name, v.ID)
}
if !confirmDeletion(len(toDelete)) {
fmt.Println("Cancelled")
return
}
fmt.Println("\nDeleting versions...")
deleted := 0
for _, v := range toDelete {
if err := client.deleteVersion(packageName, packageType, v.ID); err != nil {
fmt.Printf("Failed to delete %s: %v\n", v.Name, err)
} else {
fmt.Printf("Deleted %s\n", v.Name)
deleted++
}
}
fmt.Printf("\nDeleted %d/%d versions\n", deleted, len(toDelete))
}