From 50200abc0cc8e1e7bf2702f862f74137a1f56624 Mon Sep 17 00:00:00 2001 From: eyedeekay Date: Fri, 9 May 2025 22:15:44 -0400 Subject: [PATCH] sync file generator --- cmd/github-sync/main.go | 114 ++++++++++++++++++++++++++++++++++++++ go.mod | 18 ++++++ go.sum | 34 ++++++++++++ pkg/config/config.go | 108 ++++++++++++++++++++++++++++++++++++ pkg/git/ops.go | 4 +- pkg/github/client.go | 4 +- pkg/logger/logger.go | 49 ++++++++++++++++ pkg/workflow/generator.go | 6 +- 8 files changed, 330 insertions(+), 7 deletions(-) create mode 100644 cmd/github-sync/main.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 pkg/config/config.go diff --git a/cmd/github-sync/main.go b/cmd/github-sync/main.go new file mode 100644 index 0000000..9b4c4d9 --- /dev/null +++ b/cmd/github-sync/main.go @@ -0,0 +1,114 @@ +// Package main provides the entry point for the gh-mirror application. +package main + +import ( + "context" + "fmt" + "os" + "os/signal" + "syscall" + + "github.com/go-i2p/go-github-sync/pkg/config" + "github.com/go-i2p/go-github-sync/pkg/git" + "github.com/go-i2p/go-github-sync/pkg/github" + "github.com/go-i2p/go-github-sync/pkg/logger" + "github.com/go-i2p/go-github-sync/pkg/workflow" + "github.com/spf13/cobra" +) + +func main() { + log := logger.New(false) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + // Setup signal handling + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt, syscall.SIGTERM) + go func() { + <-c + log.Info("Received termination signal, shutting down...") + cancel() + os.Exit(1) + }() + + rootCmd := &cobra.Command{ + Use: "gh-mirror", + Short: "GitHub Mirror Sync Tool", + Long: "Tool for generating GitHub Actions workflow to sync external repositories to GitHub mirrors", + RunE: func(cmd *cobra.Command, args []string) error { + return run(ctx, log) + }, + } + + // Add flags + config.AddFlags(rootCmd) + + if err := rootCmd.Execute(); err != nil { + log.Error("Command execution failed", "error", err) + os.Exit(1) + } +} + +func run(ctx context.Context, log *logger.Logger) error { + // Parse configuration + cfg, err := config.Load() + if err != nil { + return fmt.Errorf("failed to load configuration: %w", err) + } + log.Info("Configuration loaded successfully", + "primary_repo", cfg.PrimaryRepo, + "mirror_repo", cfg.MirrorRepo, + "primary_branch", cfg.PrimaryBranch, + "mirror_branch", cfg.MirrorBranch, + "sync_interval", cfg.SyncInterval) + + // Update logger verbosity if needed + if cfg.Verbose { + log = logger.New(true) + } + + // Validate Git repositories + gitClient := git.NewClient(log) + err = gitClient.ValidateRepos(ctx, cfg) + if err != nil { + return fmt.Errorf("repository validation failed: %w", err) + } + log.Info("Git repositories validated successfully") + + // Setup GitHub client + githubClient, err := github.NewClient(ctx, cfg, log) + if err != nil { + return fmt.Errorf("failed to create GitHub client: %w", err) + } + log.Info("GitHub client initialized successfully") + + // Generate workflow file + generator := workflow.NewGenerator(cfg, log) + workflowYAML, err := generator.Generate() + if err != nil { + return fmt.Errorf("failed to generate workflow file: %w", err) + } + log.Info("Workflow file generated successfully") + + // Setup GitHub repository (optional) + if cfg.SetupWorkflow { + err = githubClient.SetupWorkflow(ctx, workflowYAML) + if err != nil { + return fmt.Errorf("failed to setup GitHub workflow: %w", err) + } + log.Info("GitHub workflow set up successfully") + } else { + // Write workflow to stdout or file + if cfg.OutputFile != "" { + err = os.WriteFile(cfg.OutputFile, []byte(workflowYAML), 0644) + if err != nil { + return fmt.Errorf("failed to write workflow to file: %w", err) + } + log.Info("Workflow written to file", "file", cfg.OutputFile) + } else { + fmt.Println(workflowYAML) + } + } + + return nil +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..acfcb6d --- /dev/null +++ b/go.mod @@ -0,0 +1,18 @@ +module github.com/go-i2p/go-github-sync + +go 1.24.2 + +require ( + github.com/google/go-github/v61 v61.0.0 + github.com/spf13/cobra v1.9.1 + go.uber.org/zap v1.27.0 + golang.org/x/oauth2 v0.30.0 + gopkg.in/yaml.v3 v3.0.1 +) + +require ( + github.com/google/go-querystring v1.1.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/spf13/pflag v1.0.6 // indirect + go.uber.org/multierr v1.10.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..1fd2335 --- /dev/null +++ b/go.sum @@ -0,0 +1,34 @@ +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-github/v61 v61.0.0 h1:VwQCBwhyE9JclCI+22/7mLB1PuU9eowCXKY5pNlu1go= +github.com/google/go-github/v61 v61.0.0/go.mod h1:0WR+KmsWX75G2EbpyGsGmradjo3IiciuI4BmdVCobQY= +github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= +github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= +github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= +github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= +go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= +golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/pkg/config/config.go b/pkg/config/config.go new file mode 100644 index 0000000..6c63699 --- /dev/null +++ b/pkg/config/config.go @@ -0,0 +1,108 @@ +// Package config handles the configuration settings for the GitHub mirror sync tool. +package config + +import ( + "fmt" + "os" + "strings" + + "github.com/spf13/cobra" +) + +// Config holds the application configuration. +type Config struct { + // GitHub token for authentication + GithubToken string + + // Repository URLs + PrimaryRepo string + MirrorRepo string + + // Branch names + PrimaryBranch string + MirrorBranch string + + // Synchronization settings + SyncInterval string + ForceSync bool + + // Output configuration + OutputFile string + SetupWorkflow bool + Verbose bool +} + +var ( + config Config + + // Flags + primaryRepo string + mirrorRepo string + primaryBranch string + mirrorBranch string + syncInterval string + forceSync bool + outputFile string + setupWorkflow bool + verbose bool +) + +// AddFlags adds the configuration flags to the given command. +func AddFlags(cmd *cobra.Command) { + cmd.Flags().StringVarP(&primaryRepo, "primary", "p", "", "Primary repository URL (required)") + cmd.Flags().StringVarP(&mirrorRepo, "mirror", "m", "", "GitHub mirror repository URL (required)") + cmd.Flags().StringVar(&primaryBranch, "primary-branch", "main", "Primary repository branch name") + cmd.Flags().StringVar(&mirrorBranch, "mirror-branch", "main", "GitHub mirror repository branch name") + cmd.Flags().StringVarP(&syncInterval, "interval", "i", "hourly", "Sync interval (hourly, daily, weekly)") + cmd.Flags().BoolVar(&forceSync, "force", true, "Force sync by overwriting mirror with primary content") + cmd.Flags().StringVarP(&outputFile, "output", "o", "", "Output file for workflow YAML (writes to stdout if not specified)") + cmd.Flags().BoolVar(&setupWorkflow, "setup", false, "Automatically setup the workflow in the GitHub repository") + cmd.Flags().BoolVarP(&verbose, "verbose", "v", false, "Enable verbose logging") + + cmd.MarkFlagRequired("primary") + cmd.MarkFlagRequired("mirror") +} + +// Load parses the flags and environment variables to build the configuration. +func Load() (*Config, error) { + // Get GitHub token from environment + githubToken := os.Getenv("GH_TOKEN") + if githubToken == "" { + githubToken = os.Getenv("GITHUB_TOKEN") + } + if githubToken == "" && setupWorkflow { + return nil, fmt.Errorf("GitHub token not found in environment (GH_TOKEN or GITHUB_TOKEN) but required for --setup") + } + + // Validate repositories + if primaryRepo == "" { + return nil, fmt.Errorf("primary repository URL is required") + } + if mirrorRepo == "" { + return nil, fmt.Errorf("mirror repository URL is required") + } + + // Validate sync interval + switch strings.ToLower(syncInterval) { + case "hourly", "daily", "weekly": + // valid + default: + return nil, fmt.Errorf("invalid sync interval: %s (must be hourly, daily, or weekly)", syncInterval) + } + + // Set the values in the config struct + config = Config{ + GithubToken: githubToken, + PrimaryRepo: primaryRepo, + MirrorRepo: mirrorRepo, + PrimaryBranch: primaryBranch, + MirrorBranch: mirrorBranch, + SyncInterval: syncInterval, + ForceSync: forceSync, + OutputFile: outputFile, + SetupWorkflow: setupWorkflow, + Verbose: verbose, + } + + return &config, nil +} diff --git a/pkg/git/ops.go b/pkg/git/ops.go index 22885ba..1f46fad 100644 --- a/pkg/git/ops.go +++ b/pkg/git/ops.go @@ -9,8 +9,8 @@ import ( "strings" "time" - "github.com/go-i2p/go-gh-mirror/pkg/config" - "github.com/go-i2p/go-gh-mirror/pkg/logger" + "github.com/go-i2p/go-github-sync/pkg/config" + "github.com/go-i2p/go-github-sync/pkg/logger" ) // Client provides Git repository validation and operations. diff --git a/pkg/github/client.go b/pkg/github/client.go index 2ca676b..e18b497 100644 --- a/pkg/github/client.go +++ b/pkg/github/client.go @@ -11,8 +11,8 @@ import ( "github.com/google/go-github/v61/github" "golang.org/x/oauth2" - "github.com/go-i2p/go-gh-mirror/pkg/config" - "github.com/go-i2p/go-gh-mirror/pkg/logger" + "github.com/go-i2p/go-github-sync/pkg/config" + "github.com/go-i2p/go-github-sync/pkg/logger" ) const ( diff --git a/pkg/logger/logger.go b/pkg/logger/logger.go index e69de29..8f4a45d 100644 --- a/pkg/logger/logger.go +++ b/pkg/logger/logger.go @@ -0,0 +1,49 @@ +// Package logger provides structured logging functionality. +package logger + +import ( + "os" + + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +// Logger wraps zap.Logger to provide a simpler interface. +type Logger struct { + *zap.SugaredLogger +} + +// New creates a new Logger instance. +func New(debug bool) *Logger { + level := zapcore.InfoLevel + if debug { + level = zapcore.DebugLevel + } + + encoderConfig := zapcore.EncoderConfig{ + TimeKey: "time", + LevelKey: "level", + NameKey: "logger", + CallerKey: "caller", + MessageKey: "msg", + StacktraceKey: "stacktrace", + LineEnding: zapcore.DefaultLineEnding, + EncodeLevel: zapcore.CapitalLevelEncoder, + EncodeTime: zapcore.ISO8601TimeEncoder, + EncodeDuration: zapcore.SecondsDurationEncoder, + EncodeCaller: zapcore.ShortCallerEncoder, + } + + core := zapcore.NewCore( + zapcore.NewConsoleEncoder(encoderConfig), + zapcore.Lock(os.Stdout), + level, + ) + + return &Logger{zap.New(core).Sugar()} +} + +// With adds structured context to the logger. +func (l *Logger) With(args ...interface{}) *Logger { + return &Logger{l.SugaredLogger.With(args...)} +} diff --git a/pkg/workflow/generator.go b/pkg/workflow/generator.go index 84e5d5a..f736206 100644 --- a/pkg/workflow/generator.go +++ b/pkg/workflow/generator.go @@ -8,8 +8,8 @@ import ( "gopkg.in/yaml.v3" - "github.com/go-i2p/go-gh-mirror/pkg/config" - "github.com/go-i2p/go-gh-mirror/pkg/logger" + "github.com/go-i2p/go-github-sync/pkg/config" + "github.com/go-i2p/go-github-sync/pkg/logger" ) // Generator generates GitHub Actions workflow files. @@ -187,7 +187,7 @@ git push origin {{.MirrorBranch}}` // addComments adds explanatory comments to the YAML. func addComments(yaml string) string { header := `# GitHub Actions workflow file to sync an external repository to this GitHub mirror. -# This file was automatically generated by go-gh-mirror. +# This file was automatically generated by go-github-sync. # # The workflow does the following: # - Runs on a scheduled basis (and can also be triggered manually)