diff options
| author | Paul Buetow <paul@buetow.org> | 2025-08-16 09:39:09 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2025-08-16 09:39:09 +0300 |
| commit | 9603960eeea6b06c5184850c2c2af7d257c77fdd (patch) | |
| tree | bbf495e79ae12390bb91019a770fe598fbb89785 | |
| parent | ab561e848dd7ca10497da4ef3cfba9fa02ac48d0 (diff) | |
feat(release): add repo/tag skip_releases and logs; bump version to 0.8.7v0.8.7
| -rw-r--r-- | AGENTS.md | 39 | ||||
| -rw-r--r-- | CLAUDE.md | 110 | ||||
| -rw-r--r-- | doc/configuration.md | 15 | ||||
| -rw-r--r-- | gitsyncer.example.json | 7 | ||||
| -rw-r--r-- | internal/cli/release.go | 110 | ||||
| -rw-r--r-- | internal/config/config.go | 34 | ||||
| -rw-r--r-- | internal/version/version.go | 2 |
7 files changed, 165 insertions, 152 deletions
diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..81f789a --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,39 @@ +# Repository Guidelines + +## Project Structure & Module Organization +- `cmd/gitsyncer/`: CLI entrypoint (`main.go`). +- `internal/`: private packages — `cli/` (flags/handlers), `sync/` (core git ops), `config/`, `github/`, `codeberg/`, `release/`, `showcase/`, `state/`, `version/`, `cmd/` (cobra wiring). +- `test/`: integration tests (`run_integration_tests.sh`, `test_*.sh`) and local configs. +- `doc/`: architecture, API, and development docs. `dist/`: build artifacts. + +## Build, Test, and Development Commands +- Build (current platform): `task build` or `go build -o gitsyncer ./cmd/gitsyncer`. +- Cross-compile: `task build-all` (linux/darwin/windows variants also available). +- Run binary: `./gitsyncer --help` or `task run -- --version`. +- Unit tests (when present): `task test` or `go test ./...`. +- Integration tests: `cd test && ./run_integration_tests.sh`. +- Format/lint: `task fmt`, `task vet`, `task lint` (golangci-lint). +- Tidy modules: `task mod-tidy`. + +## Coding Style & Naming Conventions +- Go formatting: enforce `gofmt`/`go fmt`; prefer `goimports` in your editor. +- Naming: exported identifiers in CamelCase; acronyms ALLCAPS (ID, URL, API). +- Errors: wrap with context using `%w` (`fmt.Errorf("context: %w", err)`). +- Interfaces: accept interfaces, return concrete types where practical. + +## Testing Guidelines +- Frameworks: standard `testing` for unit tests; integration via `test/*.sh`. +- Add unit tests alongside packages (e.g., `internal/sync/sync_test.go`). +- Run full suite locally: `go test ./...` then `cd test && ./run_integration_tests.sh`. +- Prefer meaningful test names: `TestFeature_Behavior` and `test_*.sh` for scripts. + +## Commit & Pull Request Guidelines +- Commits: Conventional Commits format — `type(scope): summary`. + - Examples: `feat(sync): add bidirectional mode`, `fix(config): validate organizations`. +- PRs: clear description, linked issues, testing notes (unit + integration), and updated docs (`doc/` or `README.md`) when user-visible. +- Keep changes focused; run `task fmt vet lint` before submitting. + +## Security & Configuration Tips +- Configuration lives at `~/.config/gitsyncer/config.json` (see `gitsyncer.example.json`). Do not commit secrets. +- Use `--config` and `--work-dir` to target isolated test setups. +- Debugging: set `GITSYNCER_DEBUG=1` to enable extra logs where supported. diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index 9bcd678..0000000 --- a/CLAUDE.md +++ /dev/null @@ -1,110 +0,0 @@ -# CLAUDE.md - -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. - -## Development Commands - -Essential commands for development: - -```bash -# Build the binary (using go-task if installed) -task # or: go build -o gitsyncer ./cmd/gitsyncer - -# Build for all platforms -task build-all - -# Run the application -task run # or: ./gitsyncer - -# Run tests -task test - -# Format code -task fmt - -# Clean build artifacts -task clean -``` - -## Usage Examples - -```bash -# Show version -gitsyncer version - -# Delete a repository from all configured organizations (with confirmation) -gitsyncer manage delete-repo <repository-name> - -# Manually check for version tags without releases -gitsyncer release check - -# Disable automatic release checking during sync operations -gitsyncer sync all --no-releases - -# Automatically create releases without confirmation prompts (AI notes enabled by default) -gitsyncer release create --auto - -# Create releases without AI notes -gitsyncer release create --auto --no-ai-notes - -# Use aichat instead of claude for AI release notes -gitsyncer release create --auto --ai-tool aichat - -# Generate showcase using aichat for project descriptions -gitsyncer showcase --ai-tool aichat -``` - -Note: Release checking is enabled by default after sync operations. It will check for version tags (formats: vX.Y.Z, vX.Y, vX, X.Y.Z, X.Y, X) that don't have corresponding releases on GitHub/Codeberg and prompt for confirmation before creating them. - -Note: The Taskfile.yaml is configured for [go-task](https://taskfile.dev/). Install with: -```bash -# macOS -brew install go-task - -# Linux -sh -c "$(curl --location https://taskfile.dev/install.sh)" -- -d -b ~/.local/bin - -# Or use Go directly if task is not installed -go build -o gitsyncer ./cmd/gitsyncer -``` - -## Project Structure - -``` -gitsyncer/ -├── cmd/ -│ └── gitsyncer/ -│ └── main.go # Main entry point with CLI flags -├── internal/ -│ └── version/ -│ └── version.go # Version information -├── go.mod # Go module definition -└── Taskfile.yaml # Task automation (go-task) -``` - -This follows the standard Go project layout with: -- `cmd/` for application entry points -- `internal/` for private application code -- Root directory for public libraries (if any) - -## Architecture - -The application currently provides: -- Version information system (internal/version) -- CLI flag parsing for --version -- Automatic release checking and creation (internal/release) -- Repository syncing across GitHub, Codeberg, and other platforms -- Project showcase generation - -## Next Steps - -The project needs: -1. Support for other platforms (GitLab, Gitea, etc.) -2. Webhook support for automatic syncing -3. Conflict resolution strategies -4. Better handling of large repositories - -## Release Process - -- When releasing a version, increment the version in version.go, commit all changes to git and push. and tag the version and push to git. -- Gitsyncer will automatically detect the new version tag and prompt to create releases on GitHub and Codeberg using the tokens from your gitsyncer configuration file.
\ No newline at end of file diff --git a/doc/configuration.md b/doc/configuration.md index e3b94cb..25a425f 100644 --- a/doc/configuration.md +++ b/doc/configuration.md @@ -65,6 +65,19 @@ Array of repository names to sync. If empty, use `--sync-codeberg-public` or `-- #### exclude_branches (optional) Array of regex patterns for branches to exclude from synchronization. +#### skip_releases (optional) +Map of repository names to an array of tag names for which releases should not be created on any platform (GitHub and Codeberg). Useful to suppress auto-release for specific historical tags. + +Example: +```json +{ + "skip_releases": { + "fapi": ["0.0.1", "0.0.2"], + "another-repo": ["v1.0.0"] + } +} +``` + ## Examples ### Minimal Configuration @@ -325,4 +338,4 @@ ERROR: Token test failed: authentication failed (401) **Solution**: - Verify token is correct and not expired - Check token has required permissions -- Ensure no extra whitespace in token
\ No newline at end of file +- Ensure no extra whitespace in token diff --git a/gitsyncer.example.json b/gitsyncer.example.json index abe3d78..39984b4 100644 --- a/gitsyncer.example.json +++ b/gitsyncer.example.json @@ -17,5 +17,8 @@ "gitsyncer", "another-repo", "yet-another-repo" - ] -}
\ No newline at end of file + ], + "skip_releases": { + "fapi": ["0.0.1"] + } +} diff --git a/internal/cli/release.go b/internal/cli/release.go index e05cbdd..848d28a 100644 --- a/internal/cli/release.go +++ b/internal/cli/release.go @@ -151,9 +151,9 @@ func HandleCheckReleasesForRepos(cfg *config.Config, flags *Flags, repositories fmt.Println("No Codeberg organization found in config") } - // Process the specified repositories - for _, repoName := range repositories { - fmt.Printf("\nChecking releases for repository: %s\n", repoName) + // Process the specified repositories + for _, repoName := range repositories { + fmt.Printf("\nChecking releases for repository: %s\n", repoName) // Check if the repository is cloned locally repoPath := filepath.Join(flags.WorkDir, repoName) @@ -174,41 +174,84 @@ func HandleCheckReleasesForRepos(cfg *config.Config, flags *Flags, repositories continue } - fmt.Printf(" Found %d version tags: %s\n", len(localTags), strings.Join(localTags, ", ")) + fmt.Printf(" Found %d version tags: %s\n", len(localTags), strings.Join(localTags, ", ")) + // Log configured skip rules for this repo, if any + if cfg.SkipReleases != nil { + if skipTags, ok := cfg.SkipReleases[repoName]; ok && len(skipTags) > 0 { + fmt.Printf(" Config skip_releases for %s: %s\n", repoName, strings.Join(skipTags, ", ")) + } + } // Check GitHub releases if GitHub is configured var missingGitHub []string githubOrg := cfg.FindGitHubOrg() - if githubOrg != nil && githubOrg.Name != "" { - githubReleases, err := releaseManager.GetGitHubReleases(githubOrg.Name, repoName) - if err != nil { - fmt.Printf(" Error checking GitHub releases: %v\n", err) - } else { - missingGitHub = releaseManager.FindMissingReleases(localTags, githubReleases) - if len(missingGitHub) > 0 { - fmt.Printf(" Missing GitHub releases: %s\n", strings.Join(missingGitHub, ", ")) - } - } - } + if githubOrg != nil && githubOrg.Name != "" { + githubReleases, err := releaseManager.GetGitHubReleases(githubOrg.Name, repoName) + if err != nil { + fmt.Printf(" Error checking GitHub releases: %v\n", err) + } else { + missingGitHub = releaseManager.FindMissingReleases(localTags, githubReleases) + // Filter out tags that should be skipped per config + if len(missingGitHub) > 0 { + var filtered []string + var skipped []string + for _, t := range missingGitHub { + if cfg.ShouldSkipRelease(repoName, t) { + skipped = append(skipped, t) + } else { + filtered = append(filtered, t) + } + } + if len(skipped) > 0 { + fmt.Printf(" Skipping GitHub releases per config for tags: %s\n", strings.Join(skipped, ", ")) + } + missingGitHub = filtered + if len(missingGitHub) > 0 { + fmt.Printf(" Missing GitHub releases: %s\n", strings.Join(missingGitHub, ", ")) + } + } + } + } // Check Codeberg releases if Codeberg is configured var missingCodeberg []string codebergOrg := cfg.FindCodebergOrg() - if codebergOrg != nil && codebergOrg.Name != "" { - codebergReleases, err := releaseManager.GetCodebergReleases(codebergOrg.Name, repoName) - if err != nil { - fmt.Printf(" Error checking Codeberg releases: %v\n", err) - } else { - missingCodeberg = releaseManager.FindMissingReleases(localTags, codebergReleases) - if len(missingCodeberg) > 0 { - fmt.Printf(" Missing Codeberg releases: %s\n", strings.Join(missingCodeberg, ", ")) - } - } - } + if codebergOrg != nil && codebergOrg.Name != "" { + codebergReleases, err := releaseManager.GetCodebergReleases(codebergOrg.Name, repoName) + if err != nil { + fmt.Printf(" Error checking Codeberg releases: %v\n", err) + } else { + missingCodeberg = releaseManager.FindMissingReleases(localTags, codebergReleases) + // Filter out tags that should be skipped per config + if len(missingCodeberg) > 0 { + var filtered []string + var skipped []string + for _, t := range missingCodeberg { + if cfg.ShouldSkipRelease(repoName, t) { + skipped = append(skipped, t) + } else { + filtered = append(filtered, t) + } + } + if len(skipped) > 0 { + fmt.Printf(" Skipping Codeberg releases per config for tags: %s\n", strings.Join(skipped, ", ")) + } + missingCodeberg = filtered + if len(missingCodeberg) > 0 { + fmt.Printf(" Missing Codeberg releases: %s\n", strings.Join(missingCodeberg, ", ")) + } + } + } + } // Create missing releases with confirmation - if len(missingGitHub) > 0 && githubOrg != nil { - for _, tag := range missingGitHub { + if len(missingGitHub) > 0 && githubOrg != nil { + for _, tag := range missingGitHub { + // Skip if configured to skip this repo/tag + if cfg.ShouldSkipRelease(repoName, tag) { + fmt.Printf(" Skipping GitHub release for %s:%s per config skip_releases\n", repoName, tag) + continue + } // Get commits for this tag commits, err := releaseManager.GetCommitsSinceTag(repoPath, "", tag) if err != nil { @@ -281,8 +324,13 @@ func HandleCheckReleasesForRepos(cfg *config.Config, flags *Flags, repositories } } - if len(missingCodeberg) > 0 && codebergOrg != nil { - for _, tag := range missingCodeberg { + if len(missingCodeberg) > 0 && codebergOrg != nil { + for _, tag := range missingCodeberg { + // Skip if configured to skip this repo/tag + if cfg.ShouldSkipRelease(repoName, tag) { + fmt.Printf(" Skipping Codeberg release for %s:%s per config skip_releases\n", repoName, tag) + continue + } // Get commits for this tag commits, err := releaseManager.GetCommitsSinceTag(repoPath, "", tag) if err != nil { @@ -564,4 +612,4 @@ func saveAIReleaseNotesCache(cacheFile string, cache map[string]string) error { // Don't print on every save since we save after each generation return nil -}
\ No newline at end of file +} diff --git a/internal/config/config.go b/internal/config/config.go index 86c474f..dce5526 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -19,11 +19,14 @@ type Organization struct { // Config holds the application configuration type Config struct { - Organizations []Organization `json:"organizations"` - Repositories []string `json:"repositories,omitempty"` - ExcludeBranches []string `json:"exclude_branches,omitempty"` // Regex patterns for branches to exclude - WorkDir string `json:"work_dir,omitempty"` // Working directory for cloning repositories - ExcludeFromShowcase []string `json:"exclude_from_showcase,omitempty"` // Repository names to exclude from showcase + Organizations []Organization `json:"organizations"` + Repositories []string `json:"repositories,omitempty"` + ExcludeBranches []string `json:"exclude_branches,omitempty"` // Regex patterns for branches to exclude + WorkDir string `json:"work_dir,omitempty"` // Working directory for cloning repositories + ExcludeFromShowcase []string `json:"exclude_from_showcase,omitempty"` // Repository names to exclude from showcase + // SkipReleases maps a repository name to a list of tag names for which + // releases should NOT be created on any platform (GitHub/Codeberg) + SkipReleases map[string][]string `json:"skip_releases,omitempty"` } // Load reads and parses the configuration file @@ -99,7 +102,25 @@ func (c *Config) Validate() error { } } - return nil + return nil +} + +// ShouldSkipRelease returns true if the configuration specifies that +// the given repo/tag combination should not have a release created. +func (c *Config) ShouldSkipRelease(repo, tag string) bool { + if c == nil || c.SkipReleases == nil { + return false + } + tags, ok := c.SkipReleases[repo] + if !ok { + return false + } + for _, t := range tags { + if t == tag { + return true + } + } + return false } // GetGitURL returns the git URL for an organization @@ -157,4 +178,3 @@ func (o *Organization) IsSSH() bool { return !o.IsGitHub() && !o.IsCodeberg() && !strings.HasPrefix(o.Host, "file://") && (strings.Contains(o.Host, "@") || strings.Contains(o.Host, ":")) } - diff --git a/internal/version/version.go b/internal/version/version.go index efce7d6..93b9ce8 100644 --- a/internal/version/version.go +++ b/internal/version/version.go @@ -7,7 +7,7 @@ import ( var ( // Version is the current version of gitsyncer - Version = "0.8.6" + Version = "0.8.7" // GitCommit is the git commit hash at build time GitCommit = "unknown" |
