212 lines
4.9 KiB
Go
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))
|
|
}
|