Compare commits
	
		
			2 Commits
		
	
	
		
			11335d572a
			...
			0e3cffd78d
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 0e3cffd78d | ||
|  | f1a649641f | 
							
								
								
									
										15
									
								
								.vscode/launch.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								.vscode/launch.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,15 @@ | |||||||
|  | { | ||||||
|  |     // Use IntelliSense to learn about possible attributes. | ||||||
|  |     // Hover to view descriptions of existing attributes. | ||||||
|  |     // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 | ||||||
|  |     "version": "0.2.0", | ||||||
|  |     "configurations": [ | ||||||
|  |         { | ||||||
|  |             "name": "Launch Package", | ||||||
|  |             "type": "go", | ||||||
|  |             "request": "launch", | ||||||
|  |             "mode": "auto", | ||||||
|  |             "program": "${fileDirname}" | ||||||
|  |         } | ||||||
|  |     ] | ||||||
|  | } | ||||||
							
								
								
									
										211
									
								
								cmd/git-package-cleanup/main.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										211
									
								
								cmd/git-package-cleanup/main.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,211 @@ | |||||||
|  | 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)) | ||||||
|  | } | ||||||
		Loading…
	
		Reference in New Issue
	
	Block a user