Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
138 changes: 13 additions & 125 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# GitHub Repository Manager (v1.0.2)
# GitHub Repository Manager

[![Build and Release](https://github.com/utahcon/github-pokemon/actions/workflows/release.yml/badge.svg)](https://github.com/utahcon/github-pokemon/actions/workflows/release.yml)

Expand Down Expand Up @@ -103,38 +103,6 @@ github-repo-manager --org "my-organization" --path "./repos" --verbose
- **GitHub API**: Uses a personal access token via the `GITHUB_TOKEN` environment variable
- **Git operations**: Uses SSH keys configured in your system

### Version Management

This project uses a flexible version management approach:

- During development: Uses branch-based versions (e.g., `1.0.0-feature_branch`)
- For releases: Uses Semantic Versioning (e.g., `1.0.0`)

Version information is stored in a standalone `VERSION` file in the project root.

#### Setting Development Versions

To set a development version based on your branch name:

```bash
# Automatically create a version using current branch name
./scripts/set-dev-version.sh

# Specify a base version with branch suffix
./scripts/set-dev-version.sh 1.2.0
```

#### Setting Release Versions

When preparing a PR to main, update to a proper semantic version:

```bash
# Just edit the VERSION file directly
echo "1.2.0" > VERSION

# And update the CHANGELOG.md accordingly
```

### Setting Up Authentication

1. **GitHub Personal Access Token**:
Expand Down Expand Up @@ -207,106 +175,26 @@ If you encounter errors like "permission denied" or "authentication failed":

The tool will automatically detect authentication issues and provide helpful guidance.

#### Git Hook Issues
### Update Notifications

If you're having problems with Git hooks:

1. Make sure the hooks are installed: `./scripts/install-hooks.sh`
2. Check hook permissions: `ls -la .githooks/`
3. You can bypass hooks temporarily with: `git push --no-verify`
4. If hooks aren't triggering, verify Git configuration: `git config core.hooksPath`
The tool automatically checks for newer releases on GitHub when it runs. If a newer version is available, a notice is printed to stderr at the end of the run. This check runs in the background with a 5-second timeout and never blocks the main operation.

## Contributing

Contributions are welcome! Please follow these steps:

1. Fork the repository
2. Create a feature branch (`git checkout -b feature/amazing-feature`)
3. Install Git hooks (`./scripts/install-hooks.sh`)
4. Set a development version (`./scripts/set-dev-version.sh`)
5. Make your changes
6. Update CHANGELOG.md with your changes
7. Update to a proper release version when ready for a PR to main
8. Run the version check (`./scripts/check-version.sh`)
9. Commit your changes (`git commit -m 'Add some amazing feature'`)
10. Push to the branch (`git push origin feature/amazing-feature`)
11. Open a Pull Request

The CI workflow will verify that your changes include a version increment and CHANGELOG update when targeting the main branch.

### Releasing and Version Management

This project uses GitHub Actions to automatically build and release binaries. See [CHANGELOG.md](CHANGELOG.md) for version history details.

We follow [Semantic Versioning](https://semver.org/) for version numbers (MAJOR.MINOR.PATCH).

#### Version Guardrails

The main branch is protected and requires that PRs increment the version number:

1. Any PR to main must contain a proper semantic version in the `VERSION` file
2. The CHANGELOG.md must be updated with the new version
3. A GitHub Actions workflow verifies these requirements on every PR to main
4. During development, branch-based versions are allowed (e.g., `1.0.0-feature_branch`)

#### Automatic Version Synchronization

When changes are merged to main, a GitHub Actions workflow automatically:

1. Extracts the current version from `cmd/root.go`
2. Updates all version references in the documentation (README, CHANGELOG, etc.)
3. Commits and pushes these synchronized changes back to the main branch

You can also run version synchronization locally:

```bash
# Using the version from VERSION file
./scripts/sync-versions.sh

# Or specifying a version
./scripts/sync-versions.sh 1.2.3
```

#### Git Hooks for Version Management

This repository includes Git hooks to ensure version compliance before pushing:

1. Install the hooks:
```bash
./scripts/install-hooks.sh
```

2. The pre-push hook will automatically:
- Detect when you're pushing to the main branch
- Check if version-related files were modified
- Run strict version checks only when pushing to main
- Allow development branch-based versions in other branches
- Block the push to main if semantic version rules aren't followed

This helps catch version issues early, before even creating a PR.

#### Creating a Release

1. Create a feature branch:
```bash
git checkout -b feature/my-feature
```

2. Update the VERSION file with a proper semantic version and update CHANGELOG.md

3. Run the version check script to validate:
```bash
./scripts/check-version.sh
```

4. Open a PR to the main branch
3. Make your changes
4. Commit using [Conventional Commits](https://www.conventionalcommits.org/) (`feat:`, `fix:`, etc.)
5. Push to the branch (`git push origin feature/amazing-feature`)
6. Open a Pull Request

5. After merging to main, create and push a tag with the version number:
```bash
git tag v1.0.0
git push origin v1.0.0
```
### Releasing

6. The GitHub Action will automatically build binaries for all supported platforms and create a release.
This project uses [release-please](https://github.com/googleapis/release-please) for automated versioning:

7. You can also manually trigger a build using the "workflow_dispatch" event in GitHub Actions.
1. Commit messages must follow Conventional Commits
2. On merge to `main`, release-please opens/updates a release PR with version bump and CHANGELOG
3. When the release PR is merged, a Git tag is created
4. The tag triggers GoReleaser to build cross-platform binaries and create a GitHub release
5 changes: 4 additions & 1 deletion cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,10 @@ func filterNonArchived(repos []*github.Repository) []*github.Repository {
}

func runRootCommand(ctx context.Context) error {
// Start update check in background (non-blocking, 5s timeout).
token := os.Getenv("GITHUB_TOKEN")
updateCh := checkForUpdate(ctx, token)
defer printUpdateNotice(updateCh)
if noColor {
color.NoColor = true
}
Expand All @@ -271,7 +275,6 @@ func runRootCommand(ctx context.Context) error {
return fmt.Errorf("resolving target path: %w", err)
}

token := os.Getenv("GITHUB_TOKEN")
if token == "" {
return fmt.Errorf("GITHUB_TOKEN not set: set it with: export GITHUB_TOKEN=\"your-personal-access-token\"")
}
Expand Down
106 changes: 106 additions & 0 deletions cmd/update.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package cmd

import (
"context"
"fmt"
"os"
"strings"
"time"

"github.com/google/go-github/v84/github"
)

const (
repoOwner = "utahcon"
repoName = "github-pokemon"
)

type updateResult struct {
latest string
err error
}

// checkForUpdate queries the GitHub API for the latest release and returns
// the tag name if it is newer than the current version. The check respects
// the supplied context so it can be cancelled if the main work finishes first.
func checkForUpdate(ctx context.Context, token string) <-chan updateResult {
ch := make(chan updateResult, 1)
go func() {
defer close(ch)

checkCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()

client := github.NewClient(nil)
if token != "" {
client = client.WithAuthToken(token)
}

release, _, err := client.Repositories.GetLatestRelease(checkCtx, repoOwner, repoName)
if err != nil {
ch <- updateResult{err: err}
return
}

latest := strings.TrimPrefix(release.GetTagName(), "v")
if latest != "" && latest != version && isNewer(latest, version) {
ch <- updateResult{latest: latest}
}
}()
return ch
}

// isNewer reports whether latest is a higher semver than current.
// Both are expected in "MAJOR.MINOR.PATCH" form (no "v" prefix).
// Returns false if either cannot be parsed.
func isNewer(latest, current string) bool {
lParts := parseSemver(latest)
cParts := parseSemver(current)
if lParts == nil || cParts == nil {
return false
}
for i := 0; i < 3; i++ {
if lParts[i] > cParts[i] {
return true
}
if lParts[i] < cParts[i] {
return false
}
}
return false
}

// parseSemver splits a "MAJOR.MINOR.PATCH" string (ignoring any pre-release
// suffix after a hyphen) into three integers. Returns nil on failure.
func parseSemver(v string) []int {
// Strip pre-release suffix (e.g. "1.0.0-dev" -> "1.0.0")
if idx := strings.IndexByte(v, '-'); idx != -1 {
v = v[:idx]
}
parts := strings.Split(v, ".")
if len(parts) != 3 {
return nil
}
nums := make([]int, 3)
for i, p := range parts {
n := 0
for _, c := range p {
if c < '0' || c > '9' {
return nil
}
n = n*10 + int(c-'0')
}
nums[i] = n
}
return nums
}

// printUpdateNotice prints a message to stderr if a newer version is available.
func printUpdateNotice(ch <-chan updateResult) {
result, ok := <-ch
if !ok || result.err != nil || result.latest == "" {
return
}
_, _ = fmt.Fprintf(os.Stderr, "\nA newer version of github-pokemon is available: v%s (current: v%s)\n", result.latest, version)
_, _ = fmt.Fprintf(os.Stderr, "Download: https://github.com/%s/%s/releases/latest\n", repoOwner, repoName)
}