summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2025-08-16 09:39:09 +0300
committerPaul Buetow <paul@buetow.org>2025-08-16 09:39:09 +0300
commit9603960eeea6b06c5184850c2c2af7d257c77fdd (patch)
treebbf495e79ae12390bb91019a770fe598fbb89785
parentab561e848dd7ca10497da4ef3cfba9fa02ac48d0 (diff)
feat(release): add repo/tag skip_releases and logs; bump version to 0.8.7v0.8.7
-rw-r--r--AGENTS.md39
-rw-r--r--CLAUDE.md110
-rw-r--r--doc/configuration.md15
-rw-r--r--gitsyncer.example.json7
-rw-r--r--internal/cli/release.go110
-rw-r--r--internal/config/config.go34
-rw-r--r--internal/version/version.go2
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"