From d44cd83058fb8d0d590d3450b657d052232cd17f Mon Sep 17 00:00:00 2001 From: jonathanagustin <5193877+jonathanagustin@users.noreply.github.com> Date: Thu, 21 Nov 2024 07:00:52 +0000 Subject: [PATCH] fix --- .github/workflows/ci.yml | 61 ++ .github/workflows/release.yml | 180 ++++ Taskfile.yml | 23 +- build/coverage/coverage.html | 1571 +++++++++++++++++++++++++++++++++ cmd/version.go | 4 +- main.go | 114 ++- pkg/combine/combine.go | 36 +- 7 files changed, 1920 insertions(+), 69 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/release.yml create mode 100644 build/coverage/coverage.html diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..0646a37 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,61 @@ +name: Continuous Integration + +on: + push: + branches: + - develop + - feature/** + - release/** + pull_request: + branches: + - develop + - release/** + +jobs: + build-test: + runs-on: ubuntu-latest + strategy: + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + go-version: ['1.23'] + steps: + - name: Checkout Code + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Set up Go + uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0 + with: + go-version: ${{ matrix.go-version }} + + - name: Cache Go Modules + uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2 + with: + path: | + ~/.cache/go-build + ~/go/pkg/mod + key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go- + + - name: Install Dependencies + run: | + task deps + + - name: Run Linters + run: | + task lint + + - name: Run Tests + run: | + task test + + - name: Build Application + run: | + task build + + - name: Upload Build Artifacts + if: success() + uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 + with: + name: build-${{ matrix.os }} + path: build/ diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..5eb597e --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,180 @@ +name: Release + +on: + push: + tags: + - 'v*.*.*' + +jobs: + release: + runs-on: ubuntu-latest + env: + VERSION: ${{ github.ref_name }} + steps: + - name: Checkout Code + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Set up Go + uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0 + with: + go-version: '1.23' + + - name: Install Dependencies + run: | + task deps + + - name: Run Tests + run: | + task test + + - name: Build for Windows + env: + GOOS: windows + GOARCH: amd64 + run: | + task build + shell: bash + + - name: Build for Linux + env: + GOOS: linux + GOARCH: amd64 + run: | + task build + shell: bash + + - name: Build for macOS + env: + GOOS: darwin + GOARCH: amd64 + run: | + task build + shell: bash + + - name: Package Binaries + run: | + mkdir -p package/${VERSION} + + # Package Windows + cd build + zip -r ../package/${VERSION}/omnivex-${VERSION}-windows-amd64.zip omnivex.exe + cd .. + + # Package Linux + tar -czvf package/${VERSION}/omnivex-${VERSION}-linux-amd64.tar.gz -C build omnivex + + # Package macOS + tar -czvf package/${VERSION}/omnivex-${VERSION}-darwin-amd64.tar.gz -C build omnivex + + # Generate Checksums + cd package/${VERSION} + sha256sum * > checksums.txt + cd ../../ + + - name: Generate Release Notes + id: releasenotes + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + with: + script: | + const { owner, repo } = context.repo; + const tag = context.ref.replace('refs/tags/', ''); + const latestRelease = await github.rest.repos.getLatestRelease({ + owner, + repo, + }).catch(() => null); + + const since = latestRelease ? latestRelease.data.published_at : '1970-01-01T00:00:00Z'; + + const pullRequests = await github.rest.pulls.list({ + owner, + repo, + state: 'closed', + sort: 'updated', + direction: 'desc', + per_page: 100, + }); + + const mergedPRs = pullRequests.data.filter(pr => pr.merged_at && new Date(pr.merged_at) > new Date(since)); + + let releaseNotes = `## Release ${tag}\n\n### Changes\n\n`; + + if (mergedPRs.length === 0) { + releaseNotes += '- No changes\n'; + } else { + const categories = { + '🚀 Features': [], + '🐛 Bug Fixes': [], + '🛠 Maintenance': [], + 'Other': [] + }; + + mergedPRs.forEach(pr => { + const labels = pr.labels.map(label => label.name); + if (labels.includes('feature')) { + categories['🚀 Features'].push(pr); + } else if (labels.includes('bug')) { + categories['🐛 Bug Fixes'].push(pr); + } else if (labels.includes('chore') || labels.includes('refactor')) { + categories['🛠 Maintenance'].push(pr); + } else { + categories['Other'].push(pr); + } + }); + + for (const [category, prs] of Object.entries(categories)) { + if (prs.length > 0) { + releaseNotes += `### ${category}\n\n`; + prs.forEach(pr => { + releaseNotes += `- ${pr.title} @${pr.user.login} (#${pr.number})\n`; + }); + releaseNotes += `\n`; + } + } + } + + return releaseNotes; + result-encoding: string + + - name: Create GitHub Release + id: create_release + uses: actions/create-release@0cb9c9b65d5d1901c1f53e5e66eaf4afd303e70e # v1.1.4 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ github.ref }} + release_name: Release ${{ github.ref }} + body: ${{ steps.releasenotes.outputs.result }} + draft: false + prerelease: ${{ startsWith(github.ref_name, 'v') && contains(github.ref_name, '-') }} + + - name: Upload Release Assets (Windows) + uses: actions/upload-release-asset@e8f9f06c4b078e705bd2ea027f0926603fc9b4d5 # v1.0.2 + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: ./package/${{ env.VERSION }}/omnivex-${{ env.VERSION }}-windows-amd64.zip + asset_name: omnivex-${{ env.VERSION }}-windows-amd64.zip + asset_content_type: application/zip + + - name: Upload Release Assets (Linux) + uses: actions/upload-release-asset@e8f9f06c4b078e705bd2ea027f0926603fc9b4d5 # v1.0.2 + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: ./package/${{ env.VERSION }}/omnivex-${{ env.VERSION }}-linux-amd64.tar.gz + asset_name: omnivex-${{ env.VERSION }}-linux-amd64.tar.gz + asset_content_type: application/gzip + + - name: Upload Release Assets (macOS) + uses: actions/upload-release-asset@e8f9f06c4b078e705bd2ea027f0926603fc9b4d5 # v1.0.2 + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: ./package/${{ env.VERSION }}/omnivex-${{ env.VERSION }}-darwin-amd64.tar.gz + asset_name: omnivex-${{ env.VERSION }}-darwin-amd64.tar.gz + asset_content_type: application/gzip + + - name: Upload Checksums + uses: actions/upload-release-asset@e8f9f06c4b078e705bd2ea027f0926603fc9b4d5 # v1.0.2 + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: ./package/${{ env.VERSION }}/checksums.txt + asset_name: checksums.txt + asset_content_type: text/plain diff --git a/Taskfile.yml b/Taskfile.yml index 6f05bb8..cb062b7 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -171,9 +171,8 @@ tasks: deps:install-tools: desc: Install development tools - cmds: - - echo "🔧 Installing development tools..." - - task: install-tools + deps: [install-tools:godoc, install-tools:golangci-lint, install-tools:check-curl] + cmds: [] install-tools: desc: Install development tools @@ -183,8 +182,26 @@ tasks: install-tools:golangci-lint: desc: Install 'golangci-lint' cmds: + - echo "🔧 Installing 'golangci-lint'..." - go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest + install-tools:godoc: + desc: Install 'godoc' tool + cmds: + - echo "🔧 Installing 'godoc'..." + - go install golang.org/x/tools/cmd/godoc@latest + + install-tools:check-curl: + desc: Ensure 'curl' is installed + cmds: + - | + if ! command -v curl >/dev/null 2>&1; then + echo "❌ 'curl' is not installed. Please install it using your package manager." + exit 1 + else + echo "✅ 'curl' is installed." + fi + update-deps: desc: Update project dependencies prompt: This will update all dependencies. Continue? diff --git a/build/coverage/coverage.html b/build/coverage/coverage.html new file mode 100644 index 0000000..83adf5a --- /dev/null +++ b/build/coverage/coverage.html @@ -0,0 +1,1571 @@ + + + + + + cmd: Go Coverage Report + + + +
+ +
+ not tracked + + not covered + covered + +
+
+
+ + + + + + + + + + + + + +
+ + + diff --git a/cmd/version.go b/cmd/version.go index 9332c3a..3cd55d4 100644 --- a/cmd/version.go +++ b/cmd/version.go @@ -31,8 +31,8 @@ var versionCmd = &cobra.Command{ // Display version information to the user fmt.Println(v.String()) - // Optionally, log that the version command was executed - logger.Info("Executed version command", zap.String("version", v.Version), zap.String("commit", v.GitCommit)) + // Log that the version command was executed + logger.Debug("Executed version command", zap.String("version", v.Version), zap.String("commit", v.GitCommit)) }, } diff --git a/main.go b/main.go index e34ce48..75cc68d 100644 --- a/main.go +++ b/main.go @@ -2,70 +2,92 @@ package main import ( "log" - "omnivex/cmd" "os" - "strings" + "runtime/debug" + + "omnivex/cmd" "go.uber.org/zap" "go.uber.org/zap/zapcore" - "golang.org/x/term" ) -func main() { - // Default to plain text output - outputFormat := "text" +// createLogger creates and configures the application's logger +func createLogger(verbose bool) (*zap.Logger, error) { + // Configure encoder + encoderConfig := zapcore.EncoderConfig{ + TimeKey: "timestamp", + LevelKey: "level", + NameKey: "logger", + CallerKey: "caller", + FunctionKey: zapcore.OmitKey, + MessageKey: "msg", + StacktraceKey: "stacktrace", + LineEnding: zapcore.DefaultLineEnding, + EncodeLevel: zapcore.CapitalLevelEncoder, + EncodeTime: zapcore.ISO8601TimeEncoder, + EncodeDuration: zapcore.SecondsDurationEncoder, + EncodeCaller: zapcore.ShortCallerEncoder, + } - // Check for an environment variable to override the output format - if envFormat := os.Getenv("OMNIVEX_LOG_FORMAT"); envFormat != "" { - outputFormat = envFormat + // Create stdout syncer + stdout := zapcore.AddSync(os.Stdout) + + // Determine log level based on verbose flag + level := zap.InfoLevel + if verbose { + level = zap.DebugLevel } - var logger *zap.Logger - var err error + // Create console encoder and core + consoleEncoder := zapcore.NewConsoleEncoder(encoderConfig) + core := zapcore.NewCore(consoleEncoder, stdout, level) + + // Get build info for startup logging only + buildInfo, _ := debug.ReadBuildInfo() + + // Create base logger + logger := zap.New(core, + zap.AddCaller(), + zap.AddStacktrace(zapcore.ErrorLevel), + ) + + // Log startup information once + logger.Debug("Starting Omnivex", + zap.String("app_version", "1.0.0"), + zap.String("go_version", buildInfo.GoVersion), + zap.Int("pid", os.Getpid()), + zap.Bool("verbose_mode", verbose), + ) + + // Return clean logger without default fields + return logger, nil +} - // Create the logger based on the chosen format - switch strings.ToLower(outputFormat) { - case "json": - logger, err = zap.NewProduction(zap.Fields( - zap.String("appName", "Omnivex"), - zap.String("appVersion", "1.0.0"), - )) - case "text", "": // Treat empty string as text - config := zap.NewDevelopmentConfig() - config.EncoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder // Add color for text output - logger, err = config.Build() - default: - log.Fatalf("Invalid log format: %s. Supported formats: json, text", outputFormat) +func main() { + // Parse verbose flag + verbose := false + for _, arg := range os.Args[1:] { + if arg == "--verbose" || arg == "-v" { + verbose = true + break + } } + // Initialize logger + logger, err := createLogger(verbose) if err != nil { log.Fatalf("Failed to initialize logger: %v", err) } defer func() { - if term.IsTerminal(int(os.Stderr.Fd())) || isRegularFile(os.Stderr) { - if syncErr := logger.Sync(); syncErr != nil { - lowerErr := strings.ToLower(syncErr.Error()) - if !strings.Contains(lowerErr, "invalid argument") { - log.Printf("Logger sync failed: %v", syncErr) - } - } - } + _ = logger.Sync() }() - logger.Info("Omnivex application started", zap.String("logFormat", outputFormat)) // Log the format - + // Execute root command if err := cmd.Execute(logger); err != nil { - logger.Fatal("Omnivex execution failed", zap.Error(err)) - } - - logger.Info("Omnivex application finished successfully") -} - -// isRegularFile checks if the given file is a regular file. -func isRegularFile(f *os.File) bool { - fileInfo, err := f.Stat() - if err != nil { - return false + logger.Error("Application execution failed", + zap.Error(err), + zap.String("command", os.Args[0]), + ) + os.Exit(1) } - return fileInfo.Mode().IsRegular() } diff --git a/pkg/combine/combine.go b/pkg/combine/combine.go index 0daacf6..3e542d7 100644 --- a/pkg/combine/combine.go +++ b/pkg/combine/combine.go @@ -51,7 +51,7 @@ type CollectedFiles struct { // ExecuteWithArgs is the main entry point for the combine package with custom arguments. func ExecuteWithArgs(args Arguments, logger *zap.Logger) error { - logger.Info("Starting combine process", zap.Strings("paths", args.Paths)) + logger.Debug("Starting combine process", zap.Strings("paths", args.Paths)) // Ensure output and tree directories exist if err := os.MkdirAll(filepath.Dir(args.Output), os.ModePerm); err != nil { @@ -74,7 +74,7 @@ func ExecuteWithArgs(args Arguments, logger *zap.Logger) error { // Compile command-line ignore patterns and add them to the ignore parser if len(args.IgnorePatterns) > 0 { gi.CompileIgnoreLines(args.IgnorePatterns...) - logger.Info("Added command-line ignore patterns", zap.Int("count", len(args.IgnorePatterns))) + logger.Debug("Added command-line ignore patterns", zap.Int("count", len(args.IgnorePatterns))) } // Combine files and generate tree structure @@ -96,7 +96,7 @@ func ExecuteWithArgs(args Arguments, logger *zap.Logger) error { // CombineFiles orchestrates the combination of files and tree generation. func CombineFiles(args Arguments, gi IgnoreParser, logger *zap.Logger) error { - logger.Info("Starting file combination process", + logger.Debug("Starting file combination process", zap.Strings("inputPaths", args.Paths), zap.String("outputFile", args.Output), zap.Int("maxFileSizeKB", args.MaxFileSizeKB), @@ -136,7 +136,7 @@ func CombineFiles(args Arguments, gi IgnoreParser, logger *zap.Logger) error { zap.Error(err)) continue } - logger.Info("Collected files from directory", + logger.Debug("Collected files from directory", zap.String("dir", absPath), zap.Int("regularFileCount", len(collected.Regular)), zap.Int("binaryFileCount", len(collected.Binary))) @@ -174,7 +174,7 @@ func CombineFiles(args Arguments, gi IgnoreParser, logger *zap.Logger) error { } if !shouldContinue { - logger.Info("User chose to abort the combine process due to detected binary files.") + logger.Debug("User chose to abort the combine process due to detected binary files.") return nil } } @@ -184,7 +184,7 @@ func CombineFiles(args Arguments, gi IgnoreParser, logger *zap.Logger) error { return nil } - logger.Info("Starting file processing", + logger.Debug("Starting file processing", zap.Int("totalFiles", len(allFilesToProcess))) // Process files concurrently @@ -195,7 +195,7 @@ func CombineFiles(args Arguments, gi IgnoreParser, logger *zap.Logger) error { numWorkers := args.MaxWorkers if numWorkers <= 0 { numWorkers = runtime.NumCPU() - logger.Info("Adjusted worker count", + logger.Debug("Adjusted worker count", zap.Int("workers", numWorkers)) } @@ -233,7 +233,7 @@ func CombineFiles(args Arguments, gi IgnoreParser, logger *zap.Logger) error { close(results) <-done - logger.Info("All files processed", + logger.Debug("All files processed", zap.Int("processedFiles", len(combinedContents))) // Sort files for consistent output @@ -243,7 +243,7 @@ func CombineFiles(args Arguments, gi IgnoreParser, logger *zap.Logger) error { logger.Debug("Sorted processed files") // Generate tree structure - logger.Info("Generating tree structure") + logger.Debug("Generating tree structure") treeBuilder := strings.Builder{} for _, path := range args.Paths { @@ -282,14 +282,14 @@ func CombineFiles(args Arguments, gi IgnoreParser, logger *zap.Logger) error { treeContent := treeBuilder.String() // Write tree structure to tree.txt - logger.Info("Writing tree structure to tree.txt", zap.String("treeFile", args.Tree)) + logger.Debug("Writing tree structure to tree.txt", zap.String("treeFile", args.Tree)) if err := os.WriteFile(args.Tree, []byte(treeContent), 0644); err != nil { logger.Error("Failed to write tree structure", zap.String("treeFile", args.Tree), zap.Error(err)) return fmt.Errorf("failed to write tree structure: %w", err) } // Create combined.txt and write tree at the top - logger.Info("Writing combined content to combined.txt", zap.String("combinedFile", args.Output)) + logger.Debug("Writing combined content to combined.txt", zap.String("combinedFile", args.Output)) if err := os.MkdirAll(filepath.Dir(args.Output), 0755); err != nil { logger.Error("Failed to create output directory", zap.String("dir", filepath.Dir(args.Output)), @@ -766,7 +766,7 @@ func LoadIgnoreFiles(localPath, globalPath string, logger *zap.Logger) (*GitIgno zap.Error(err)) return nil, fmt.Errorf("failed to create .combineignore file: %w", err) } - gi.logger.Info("Created default .combineignore file", + gi.logger.Debug("Created default .combineignore file", zap.String("file", absLocalPath), zap.String("location", absLocalPath)) } else { @@ -789,7 +789,7 @@ func LoadIgnoreFiles(localPath, globalPath string, logger *zap.Logger) (*GitIgno zap.String("file", absGlobalPath)) if err := gi.CompileIgnoreFile(absGlobalPath); err != nil { if os.IsNotExist(err) { - gi.logger.Info("Global ignore file does not exist and will be skipped", + gi.logger.Debug("Global ignore file does not exist and will be skipped", zap.String("file", absGlobalPath)) } else { gi.logger.Error("Failed to compile global ignore file", @@ -798,7 +798,7 @@ func LoadIgnoreFiles(localPath, globalPath string, logger *zap.Logger) (*GitIgno return nil, err } } else { - gi.logger.Info("Successfully loaded global ignore file", + gi.logger.Debug("Successfully loaded global ignore file", zap.String("file", absGlobalPath)) } } @@ -816,7 +816,7 @@ func LoadIgnoreFiles(localPath, globalPath string, logger *zap.Logger) (*GitIgno zap.String("file", absLocalPath)) if err := gi.CompileIgnoreFile(absLocalPath); err != nil { if os.IsNotExist(err) { - gi.logger.Info("Local ignore file does not exist and will be skipped", + gi.logger.Debug("Local ignore file does not exist and will be skipped", zap.String("file", absLocalPath)) } else { gi.logger.Error("Failed to compile local ignore file", @@ -825,7 +825,7 @@ func LoadIgnoreFiles(localPath, globalPath string, logger *zap.Logger) (*GitIgno return nil, err } } else { - gi.logger.Info("Successfully loaded local ignore file", + gi.logger.Debug("Successfully loaded local ignore file", zap.String("file", absLocalPath)) } } @@ -865,7 +865,7 @@ func (gi *GitIgnore) CompileIgnoreFile(filePath string) error { content, err := os.ReadFile(filePath) if err != nil { if os.IsNotExist(err) { - gi.logger.Info("Ignore file does not exist and will be skipped", + gi.logger.Debug("Ignore file does not exist and will be skipped", zap.String("filePath", filePath)) return nil } @@ -900,7 +900,7 @@ func (gi *GitIgnore) CompileIgnoreFile(filePath string) error { zap.Int("lineNo", i+1)) } } - gi.logger.Info("Compiled ignore patterns from file", + gi.logger.Debug("Compiled ignore patterns from file", zap.String("filePath", filePath), zap.Int("patternCount", len(lines))) return nil