diff --git a/.gitignore b/.gitignore index b835332..00d54de 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +/.idea /venv /.env -/work \ No newline at end of file +/work diff --git a/autobuilder.go b/autobuilder.go index f1037bb..99147e0 100644 --- a/autobuilder.go +++ b/autobuilder.go @@ -1,24 +1,16 @@ package main import ( + "errors" "fmt" "log" "os" "os/exec" "path/filepath" + "strconv" + "strings" ) -func cloneRepository(repo Repository, workDir string) error { - cmd := exec.Command("git", "clone", repo.CloneURL, filepath.Join(workDir, repo.Name)) - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - err := cmd.Run() - if err != nil { - return fmt.Errorf("failed to clone %s: %w", repo.Name, err) - } - return nil -} - func main() { token := os.Getenv("TOKEN") if token == "" { @@ -47,30 +39,193 @@ func main() { log.Fatalf("Failed to get repositories: %v", err) } - success := true + failedRepos := make([]string, 0) for _, repo := range repos { - log.Printf("Checking %s", repo.FullName) - hasFile, err := client.hasPKGBUILD(repo) - - if err != nil { - log.Printf("Error checking for PKGBUILD: %v", err) - success = false + if repo.Template || strings.Contains(repo.Name, "skip-autobuild") { + // Skip template repositories continue } - - if !hasFile { - log.Println("PKGBUILD not found, skipping") - continue + wasRepoProcessed := processRepo(&client, workDir, repo) + if !wasRepoProcessed { + failedRepos = append(failedRepos, repo.Name) } - - if err := cloneRepository(repo, workDir); err != nil { - success = false - log.Printf("Error cloning repository: %v", err) - } - } - if !success { + if len(failedRepos) > 0 { + log.Println("The following repos failed to process:") + for _, repo := range failedRepos { + log.Printf(" - %s", repo) + } os.Exit(1) } } + +func processRepo(client *GiteaClient, workDir string, repo Repository) (success bool) { + log.Printf("Checking %s", repo.FullName) + hasFile, err := client.hasPKGBUILD(repo) + + if err != nil { + log.Printf("Error checking for PKGBUILD: %v", err) + return false + } + + if !hasFile { + log.Println("PKGBUILD not found, skipping") + return true + } + + repoPath, err := cloneRepository(client.Token, repo, workDir) + if err != nil { + log.Printf("Error cloning repository: %v", err) + return false + } + + // Run pre-run script (if it exists) + requirePush := false + defer func() { + if requirePush { + err = pushRepository(repoPath) + if err != nil { + log.Printf("Error pushing repository: %v", err) + success = false + } + } + }() + preRunScript := filepath.Join(repoPath, "pre-run.sh") + if _, err := os.Stat(preRunScript); !os.IsNotExist(err) { + cmd := exec.Command("/bin/sh", "./pre-run.sh") + cmd.Dir = workDir + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + err = cmd.Run() + if err != nil { + log.Printf("Error running pre-run script: %v", err) + return false + } + requirePush = true + } + + // Check if the repository is up-to-date + upToDate, err := checkUpToDate(repoPath) + if err != nil { + log.Printf("Error checking if up to date: %v", err) + return false + } + if upToDate { + log.Printf("%s is up-to-date, skipping", repo.FullName) + return true + } + + log.Printf("%s requires bumping, running script", repo.FullName) + updated, err := bumpRepository(repoPath) + if err != nil { + log.Printf("Error bumping repository: %v", err) + return false + } + if updated { + requirePush = true + } + return true +} + +func cloneRepository(token string, repo Repository, workDir string) (string, error) { + targetPath := filepath.Join(workDir, repo.Name) + cmd := exec.Command("git", "clone", fmt.Sprintf("https://seeseemelk:%s@gitea.seeseepuff.be/archlinux/%s", token, repo.Name), targetPath) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + err := cmd.Run() + if err != nil { + return "", fmt.Errorf("failed to clone %s: %w", repo.Name, err) + } + return targetPath, nil +} + +func checkUpToDate(repoPath string) (bool, error) { + log.Println("Checking if up-to-date") + cmd := exec.Command("/bin/sh", "./check-up-to-date.sh", repoPath) + output, err := cmd.CombinedOutput() + log.Println("Command output: ", string(output)) + if err != nil { + return false, fmt.Errorf("failed to inspect %s: %w", repoPath, err) + } + if strings.Index(string(output), "UP-TO-DATE") >= 0 { + return true, nil + } else if strings.Index(string(output), "OUT-OF-DATE") >= 0 { + return false, nil + } + return false, errors.New("error parsing command output") +} + +/* +Finds the line that says "pkgrel=NUMBER" in the repository's PKGBUILD file, +and increments the number by 1. +*/ +func bumpRepository(repoPath string) (bool, error) { + pkgbuildPath := filepath.Join(repoPath, "PKGBUILD") + + // Read the file + content, err := os.ReadFile(pkgbuildPath) + if err != nil { + log.Fatalf("failed to read %s: %v", pkgbuildPath, err) + } + + lines := strings.Split(string(content), "\n") + updated := false + + // Iterate through lines to find and update pkgrel + for i, line := range lines { + if strings.HasPrefix(line, "pkgrel=") { + parts := strings.SplitN(line, "=", 2) + if len(parts) == 2 { + // Parse and increment the pkgrel value + pkgrel, err := strconv.Atoi(strings.TrimSpace(parts[1])) + if err != nil { + log.Fatalf("Failed to parse pkgrel value: %v", err) + } + lines[i] = fmt.Sprintf("pkgrel=%d", pkgrel+1) + updated = true + break + } + } + } + + if !updated { + return false, fmt.Errorf("pkgrel line not found in PKGBUILD file") + } + + // Write the updated content back to the PKGBUILD file + err = os.WriteFile(pkgbuildPath, []byte(strings.Join(lines, "\n")), 0644) + if err != nil { + return false, fmt.Errorf("error writing updated PKGBUILD file: %w", err) + } + + log.Println("PKGBUILD updated, creating commit") + + // Create commit + cmd := exec.Command("git", "commit", "PKGBUILD", "-m", "Bump pkgrel") + cmd.Dir = repoPath + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + err = cmd.Run() + if err != nil { + return false, fmt.Errorf("failed to commit changes: %w", err) + } + + return true, nil +} + +func pushRepository(repoPath string) error { + // Push commit + log.Println("Pushing commit") + cmd := exec.Command("git", "push") + cmd.Dir = repoPath + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + err := cmd.Run() + if err != nil { + return fmt.Errorf("failed to push changes: %w", err) + } + + log.Println("Successfully bumped pkgrel in PKGBUILD") + return nil +} diff --git a/check-up-to-date.sh b/check-up-to-date.sh new file mode 100644 index 0000000..72d2a33 --- /dev/null +++ b/check-up-to-date.sh @@ -0,0 +1,52 @@ +#!/bin/bash + +# Exit on any error +set -e + +cd $1 + +# Source the PKGBUILD in a subshell to avoid polluting the global environment +( + source ./PKGBUILD + + # Convert pkgname to array in case it's a single string + pkgnames=("${pkgname[@]}") + + for pkg in "${pkgnames[@]}"; do + echo "Checking package: $pkg" + + if pacman -Si "$pkg" &>/dev/null; then + echo "Package '$pkg' exists in a repository." + + # Get the package build date + pkg_build_date=$(date -d "$(pacman -Si "$pkg" | grep 'Build Date' | cut -d: -f2-)" +%s) + + all_deps=("${depends[@]}" "${makedepends[@]}" "${optdepends[@]}") + + # Check each dependency + for dep in "${all_deps[@]}"; do + dep_name=$(echo "$dep" | sed 's/[<>=].*//') # Remove version constraints + echo "Querying dependency: $dep_name" + + if pacman -Si "$dep_name" &>/dev/null; then + dep_build_date=$(date -d "$(pacman -Si "$dep_name" | grep 'Build Date' | cut -d: -f2-)" +%s) + if (( dep_build_date >= pkg_build_date )); then + echo "Dependency '$dep_name' has newer or equal build date than '$pkg'." + echo "OUT-OF-DATE" + exit 0 + fi + else + echo "Dependency '$dep_name' not found in repositories. Skipping." + fi + done + + echo "All dependencies are older than package '$pkg'." + echo "UP-TO-DATE" + exit 0 + else + echo "Package '$pkg' does NOT exist in any repository." + echo "OUT-OF-DATE" + exit 0 + fi + done +) diff --git a/gitea.go b/gitea.go index 3b73a6c..2e23e99 100644 --- a/gitea.go +++ b/gitea.go @@ -14,10 +14,12 @@ type GiteaClient struct { } type Repository struct { - Name string `json:"name"` - CloneURL string `json:"clone_url"` - Owner Owner `json:"owner"` - FullName string `json:"full_name"` + Name string `json:"name"` + CloneURL string `json:"clone_url"` + Owner Owner `json:"owner"` + FullName string `json:"full_name"` + Template bool `json:"template"` + Topics []string `json:"topics"` } type Owner struct { diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..de49306 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module autobuilder + +go 1.24.2 diff --git a/run.sh b/run.sh new file mode 100755 index 0000000..828e31a --- /dev/null +++ b/run.sh @@ -0,0 +1,2 @@ +#!/usr/bin/env sh +docker run -it --platform amd64 --env-file .env --rm -v ./:/app -w /app gitea.seeseepuff.be/archlinux/archlinux