From 34cd633cca25f9b463d988581e27d912bbab8f44 Mon Sep 17 00:00:00 2001 From: Justin Linn Date: Thu, 16 Oct 2025 05:10:06 -0400 Subject: [PATCH 1/5] chore(versioning): Add semantic versioning CI action --- .github/workflows/commit-lint.yml | 161 ++++++++++++++++++ .github/workflows/release.yml | 166 ++++++++++++++++++ CHANGELOG.md | 56 ++++--- CONTRIBUTING.md | 120 ++++++++++--- Makefile | 4 +- README.md | 1 + docs/VERSIONING.md | 268 ++++++++++++++++++++++++++++++ scripts/bump-version.sh | 93 +++++++++++ scripts/update-changelog.sh | 172 +++++++++++++++++++ 9 files changed, 989 insertions(+), 52 deletions(-) create mode 100644 .github/workflows/commit-lint.yml create mode 100644 .github/workflows/release.yml create mode 100644 docs/VERSIONING.md create mode 100755 scripts/bump-version.sh create mode 100755 scripts/update-changelog.sh diff --git a/.github/workflows/commit-lint.yml b/.github/workflows/commit-lint.yml new file mode 100644 index 0000000..a9e07ad --- /dev/null +++ b/.github/workflows/commit-lint.yml @@ -0,0 +1,161 @@ +name: Commit Lint + +on: + pull_request: + branches: [main, develop] + types: [opened, synchronize, reopened, edited] + +permissions: + contents: read + pull-requests: read + +jobs: + lint-commits: + name: Validate Commit Messages + runs-on: ubuntu-latest + + steps: + - name: Harden Runner + uses: step-security/harden-runner@v2 + with: + egress-policy: audit + + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Validate commit messages + run: | + # Colors for output + RED='\033[0;31m' + GREEN='\033[0;32m' + YELLOW='\033[1;33m' + BLUE='\033[0;34m' + NC='\033[0m' # No Color + + echo -e "${BLUE}Validating commit messages...${NC}" + + # Get the base branch + BASE_REF="${{ github.event.pull_request.base.sha }}" + HEAD_REF="${{ github.event.pull_request.head.sha }}" + + # Get all commits in this PR + COMMITS=$(git log --pretty=format:"%H %s" "$BASE_REF".."$HEAD_REF") + + # Conventional commit pattern + # Types: build, chore, ci, docs, feat, fix, perf, refactor, revert, style, test + PATTERN="^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)(\([a-z0-9_-]+\))?(!)?: .+" + + INVALID_COMMITS=() + VALID_COUNT=0 + TOTAL_COUNT=0 + + while IFS= read -r line; do + [[ -z "$line" ]] && continue + + TOTAL_COUNT=$((TOTAL_COUNT + 1)) + HASH=$(echo "$line" | awk '{print $1}') + MESSAGE=$(echo "$line" | cut -d' ' -f2-) + + if [[ "$MESSAGE" =~ $PATTERN ]]; then + echo -e "${GREEN}✓${NC} $MESSAGE" + VALID_COUNT=$((VALID_COUNT + 1)) + else + echo -e "${RED}✗${NC} $MESSAGE" + INVALID_COMMITS+=("$HASH: $MESSAGE") + fi + done <<< "$COMMITS" + + echo "" + echo -e "${BLUE}Summary:${NC}" + echo -e " Total commits: $TOTAL_COUNT" + echo -e " Valid commits: ${GREEN}$VALID_COUNT${NC}" + echo -e " Invalid commits: ${RED}$((TOTAL_COUNT - VALID_COUNT))${NC}" + + # If there are invalid commits, fail the check + if [ ${#INVALID_COMMITS[@]} -gt 0 ]; then + echo "" + echo -e "${RED}❌ Found invalid commit messages:${NC}" + echo "" + for commit in "${INVALID_COMMITS[@]}"; do + echo -e " ${RED}✗${NC} $commit" + done + echo "" + echo -e "${YELLOW}Commit messages must follow the Conventional Commits specification:${NC}" + echo "" + echo -e " Format: ${BLUE}type(scope): description${NC}" + echo "" + echo -e " Types:" + echo -e " ${GREEN}feat${NC} - New feature" + echo -e " ${GREEN}fix${NC} - Bug fix" + echo -e " ${GREEN}docs${NC} - Documentation changes" + echo -e " ${GREEN}style${NC} - Code style changes (formatting, etc.)" + echo -e " ${GREEN}refactor${NC} - Code refactoring" + echo -e " ${GREEN}perf${NC} - Performance improvements" + echo -e " ${GREEN}test${NC} - Test changes" + echo -e " ${GREEN}chore${NC} - Build process or auxiliary tool changes" + echo -e " ${GREEN}ci${NC} - CI configuration changes" + echo -e " ${GREEN}build${NC} - Build system changes" + echo -e " ${GREEN}revert${NC} - Revert a previous commit" + echo "" + echo -e " Examples:" + echo -e " ${BLUE}feat(auth): Add login functionality${NC}" + echo -e " ${BLUE}fix(api): Resolve null pointer exception${NC}" + echo -e " ${BLUE}docs(readme): Update README with installation steps${NC}" + echo -e " ${BLUE}feat(auth)!: Breaking change in auth flow${NC}" + echo "" + echo -e " For more info: ${BLUE}https://www.conventionalcommits.org${NC}" + + # Add to step summary + { + echo "## ❌ Commit Message Validation Failed" + echo "" + echo "The following commits do not follow the Conventional Commits specification:" + echo "" + for commit in "${INVALID_COMMITS[@]}"; do + echo "- \`$commit\`" + done + echo "" + echo "### Required Format" + echo "" + echo "\`\`\`" + echo "type(scope): description" + echo "\`\`\`" + echo "" + echo "### Valid Types" + echo "" + echo "- \`feat\` - New feature" + echo "- \`fix\` - Bug fix" + echo "- \`docs\` - Documentation changes" + echo "- \`style\` - Code style changes" + echo "- \`refactor\` - Code refactoring" + echo "- \`perf\` - Performance improvements" + echo "- \`test\` - Test changes" + echo "- \`chore\` - Maintenance tasks" + echo "- \`ci\` - CI changes" + echo "- \`build\` - Build system changes" + echo "" + echo "### Examples" + echo "" + echo "- \`feat(auth): Add login functionality\`" + echo "- \`fix(api): Resolve null pointer exception\`" + echo "- \`docs(readme): Update README\`" + echo "" + echo "Learn more: [Conventional Commits](https://www.conventionalcommits.org)" + } >> $GITHUB_STEP_SUMMARY + + exit 1 + fi + + echo "" + echo -e "${GREEN}✓ All commit messages are valid${NC}" + + # Add success to step summary + { + echo "## ✅ Commit Message Validation Passed" + echo "" + echo "All $VALID_COUNT commit(s) follow the Conventional Commits specification." + } >> $GITHUB_STEP_SUMMARY + + exit 0 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..f4ae9aa --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,166 @@ +name: Release + +on: + push: + branches: [main] + paths-ignore: + - '**.md' + - 'docs/**' + - '.github/**' + - '!.github/workflows/release.yml' + workflow_dispatch: + inputs: + bump_type: + description: 'Version bump type' + required: false + type: choice + options: + - auto + - major + - minor + - patch + default: 'auto' + +# Prevent concurrent releases +concurrency: + group: release + cancel-in-progress: false + +permissions: + contents: write + pull-requests: read + +jobs: + release: + name: Create Release + runs-on: ubuntu-latest + # Only run if not a bot commit (to avoid release loops) + if: "!contains(github.event.head_commit.message, '[skip ci]') && !contains(github.event.head_commit.message, 'chore(release)')" + + steps: + - name: Harden Runner + uses: step-security/harden-runner@v2 + with: + egress-policy: audit + + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Configure Git + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + - name: Check if release needed + id: check_release + run: | + # Get commits since last tag + LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "") + + if [[ -z "$LAST_TAG" ]]; then + echo "No previous tag found, release needed" + echo "release_needed=true" >> $GITHUB_OUTPUT + exit 0 + fi + + # Check if there are conventional commits since last tag + COMMITS=$(git log "$LAST_TAG"..HEAD --pretty=format:"%s" --no-merges) + + if echo "$COMMITS" | grep -qE "^(feat|fix|perf|refactor)(\([a-z-]+\))?(!)?:"; then + echo "Found conventional commits, release needed" + echo "release_needed=true" >> $GITHUB_OUTPUT + else + echo "No conventional commits found, skipping release" + echo "release_needed=false" >> $GITHUB_OUTPUT + fi + + - name: Bump version + if: steps.check_release.outputs.release_needed == 'true' + id: bump_version + run: | + chmod +x scripts/bump-version.sh + + # Use manual bump type if provided, otherwise auto-detect + BUMP_TYPE="${{ github.event.inputs.bump_type || 'auto' }}" + + if [[ "$BUMP_TYPE" == "auto" ]]; then + ./scripts/bump-version.sh + else + ./scripts/bump-version.sh "$BUMP_TYPE" + fi + + - name: Update CHANGELOG + if: steps.check_release.outputs.release_needed == 'true' + run: | + chmod +x scripts/update-changelog.sh + ./scripts/update-changelog.sh "${{ steps.bump_version.outputs.new_version }}" + + - name: Extract release notes + if: steps.check_release.outputs.release_needed == 'true' + id: extract_notes + run: | + # Extract the latest version section from CHANGELOG + VERSION="${{ steps.bump_version.outputs.new_version }}" + + # Create release notes file + NOTES_FILE=$(mktemp) + + # Extract content between [VERSION] and next [VERSION] or end + awk "/^## \[$VERSION\]/ { flag=1; next } /^## \[[0-9]/ { flag=0 } flag" CHANGELOG.md > "$NOTES_FILE" + + # Set output + { + echo 'notes<> $GITHUB_OUTPUT + + - name: Commit version bump + if: steps.check_release.outputs.release_needed == 'true' + run: | + git add VERSION CHANGELOG.md + git commit -m "chore(release): bump version to ${{ steps.bump_version.outputs.new_version }} [skip ci]" + git push origin main + + - name: Create and push tag + if: steps.check_release.outputs.release_needed == 'true' + run: | + VERSION="${{ steps.bump_version.outputs.new_version }}" + git tag -a "v$VERSION" -m "Release v$VERSION" + git push origin "v$VERSION" + + - name: Create GitHub Release + if: steps.check_release.outputs.release_needed == 'true' + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: v${{ steps.bump_version.outputs.new_version }} + release_name: v${{ steps.bump_version.outputs.new_version }} + body: ${{ steps.extract_notes.outputs.notes }} + draft: false + prerelease: false + + - name: Release summary + if: steps.check_release.outputs.release_needed == 'true' + run: | + echo "## 🚀 Release v${{ steps.bump_version.outputs.new_version }}" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Previous version:** ${{ steps.bump_version.outputs.old_version }}" >> $GITHUB_STEP_SUMMARY + echo "**New version:** ${{ steps.bump_version.outputs.new_version }}" >> $GITHUB_STEP_SUMMARY + echo "**Bump type:** ${{ steps.bump_version.outputs.bump_type }}" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Release Notes" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "${{ steps.extract_notes.outputs.notes }}" >> $GITHUB_STEP_SUMMARY + + - name: Skip summary + if: steps.check_release.outputs.release_needed != 'true' + run: | + echo "## ⏭️ Release Skipped" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "No conventional commits found since last release." >> $GITHUB_STEP_SUMMARY + echo "Release will be created when feat/fix/perf/refactor commits are pushed." >> $GITHUB_STEP_SUMMARY diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ec6476..9551c86 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,54 +9,56 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- **Modular plugin architecture** - Complete rewrite from monolithic to modular structure - - 26 user-facing commands in `functions/` directory - - 8 internal helper functions in `functions/internal/` - - 3 shell completion functions in `completions/` - - Configuration management in `lib/config.zsh` - - Color support system in `lib/colors.zsh` - - Minimal 74-line autoload-based loader in `treehouse.plugin.zsh` -- **Comprehensive test suite** - 67 tests with bats-core - - 8 core tests for plugin loading and functionality - - 39 comprehensive functional tests for key commands (add, list, rm, status, lock, unlock) - - 20 autoload verification tests for remaining commands - - Shared test helpers in `tests/helpers/test_helper.bash` - - Test documentation in `tests/README.md` and `tests/commands/README.md` - - Makefile targets: `test`, `test-core`, `test-commands` - - GitHub Actions CI workflow testing on Ubuntu/macOS with Zsh 5.8/5.9 -- **All 26 commands fully functional**: - - Core: `list`, `add`, `rm`, `status`, `switch`, `open`, `main`, `prune` - - Advanced: `migrate`, `clean`, `mv`, `lock`, `unlock`, `locks` - - Archiving: `archive`, `unarchive`, `archives` - - GitHub: `pr`, `diff`, `stash-list` - - File management: `ignore`, `unignore`, `ignored`, `excludes`, `excludes-list`, `excludes-edit` +- Modular plugin architecture with complete rewrite from monolithic to modular structure +- 26 user-facing commands in `functions/` directory +- 8 internal helper functions in `functions/internal/` +- 3 shell completion functions in `completions/` +- Configuration management in `lib/config.zsh` +- Color support system in `lib/colors.zsh` +- Minimal 74-line autoload-based loader in `treehouse.plugin.zsh` +- Comprehensive test suite with 67 tests using bats-core +- Core tests for plugin loading and functionality +- Comprehensive functional tests for key commands (add, list, rm, status, lock, unlock) +- Autoload verification tests for remaining commands +- Shared test helpers in `tests/helpers/test_helper.bash` +- Test documentation in `tests/README.md` and `tests/commands/README.md` +- Makefile targets: `test`, `test-core`, `test-commands` +- GitHub Actions CI workflow testing on Ubuntu/macOS with Zsh 5.8/5.9 +- All 26 commands fully functional: list, add, rm, status, switch, open, main, prune, migrate, clean, mv, lock, + unlock, locks, archive, unarchive, archives, pr, diff, stash-list, ignore, unignore, ignored, excludes, + excludes-list, excludes-edit - CODE_OF_CONDUCT.md following Contributor Covenant 2.1 - SECURITY.md with vulnerability reporting guidelines - Issue templates for bug reports and feature requests - Pull request template with checklist - CI workflow for automated testing on Ubuntu and macOS with Zsh 5.8/5.9 - Release workflow for automated GitHub releases from version tags +- Automated semantic versioning with conventional commits +- Commit message validation in CI +- Version bump and changelog generation scripts - .editorconfig for consistent code formatting across editors - .gitattributes for consistent line endings - CITATION.cff for academic citations - .github/FUNDING.yml for sponsorship options - .github/dependabot.yml for automated dependency updates - CI badge in README.md -- **Fixed empty internal helper functions** - `_gwt_repo`, `_gwt_name`, `_gwt_path_for`, etc. - - These were accidentally left empty during modularization - - Added proper implementations with branch name sanitization (feat/test → feat-test) - .markdownlint.json with VS Code extension compatible rules - .markdownlintignore to exclude PROMPT.md - Markdown linting to CI workflow -### Changed +### Fixed -- **Complete architectural overhaul** - Migrated from monolithic 1946-line file to modular structure +- Empty internal helper functions (`_gwt_repo`, `_gwt_name`, `_gwt_path_for`, etc.) +- Added proper implementations with branch name sanitization (feat/test → feat-test) + +### Refactored + +- Complete architectural overhaul - Migrated from monolithic 1946-line file to modular structure - Plugin now uses Zsh autoload for lazy loading (improves startup performance) - Functions are individually loadable and testable - Removed duplicate `worktrees.plugin.zsh` file (renamed to `treehouse.plugin.zsh`) -### Infrastructure +### Maintenance - Established complete GitHub Actions CI/CD pipeline - Added Dependabot for keeping GitHub Actions updated diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0fee084..915edfc 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -32,21 +32,22 @@ Enhancement suggestions are tracked as GitHub issues. When creating an enhanceme 1. **Fork the repository** and create your branch from `main` 2. **Follow the coding style** used throughout the project -3. **Write clear commit messages** using Conventional Commits format with scope: - - `feat(scope): description` for new features - - `fix(scope): description` for bug fixes - - `docs(scope): description` for documentation changes - - `test(scope): description` for test additions/changes - - `refactor(scope): description` for code refactoring - - `chore(scope): description` for maintenance tasks - - Scope examples: - - `feat(gwt): add archive support` - - `fix(completion): resolve branch name completion` - - `docs(readme): update installation instructions` - - `test(core): add worktree creation tests` - - `refactor(functions): modularize helper functions` - - `chore(deps): update dependencies` +3. **Write clear commit messages** using Conventional Commits format: + - Format: `type(dir): Description` where `dir` is the component/directory/command + - `feat(dir): Description` for new features + - `fix(dir): Description` for bug fixes + - `docs(dir): Description` for documentation changes + - `test(dir): Description` for test additions/changes + - `refactor(dir): Description` for code refactoring + - `chore(dir): Description` for maintenance tasks + + Examples: + - `feat(lock): Add archive support` + - `fix(completion): Resolve branch name completion` + - `docs(readme): Update installation instructions` + - `test(core): Add worktree creation tests` + - `refactor(functions): Modularize helper functions` + - `chore(deps): Update dependencies` 4. **Update documentation** if you're changing functionality 5. **Add tests** if applicable 6. **Ensure all tests pass** before submitting @@ -84,8 +85,8 @@ gwt main # Test main command # 6. Run full test suite make test -# 7. Commit with conventional commit format (include scope) -git commit -m "feat(gwt-add): support creating from remote branches" +# 7. Commit with conventional commit format +git commit -m "feat(add): Support creating from remote branches" ``` ### Testing @@ -177,15 +178,88 @@ treehouse/ - **Git**: 2.20+ (for worktree support) - **OS**: macOS, Linux, WSL +## Commit Message Format + +This project uses [Conventional Commits](https://www.conventionalcommits.org/) for automated versioning and +changelog generation. All commits **must** follow this format: + +```text +type(dir): Description +``` + +Where `type` is the commit type, `dir` is the component/directory/command name, and `Description` is a brief summary. + +### Commit Types & Version Impact + +- **feat** - New feature (triggers MINOR version bump) +- **fix** - Bug fix (triggers PATCH version bump) +- **perf** - Performance improvement (triggers PATCH version bump) +- **refactor** - Code refactoring (triggers PATCH version bump) +- **docs** - Documentation only (no version bump) +- **test** - Test changes (no version bump) +- **chore** - Maintenance tasks (no version bump) +- **ci** - CI configuration (no version bump) +- **style** - Code formatting (no version bump) +- **build** - Build system changes (no version bump) + +### Breaking Changes + +For breaking changes, add `!` after the type/scope or include `BREAKING CHANGE:` in the footer: + +```text +feat(api)!: Remove deprecated authentication method + +BREAKING CHANGE: Old auth method removed. Use OAuth2 instead. +``` + +This triggers a **MAJOR** version bump. + +### Examples + +```text +feat(lock): Add bulk lock operation for multiple worktrees +fix(status): Resolve issue with uncommitted changes detection +perf(list): Optimize worktree listing performance +docs(readme): Update installation instructions +chore(ci): Add automated release workflow +``` + ## Release Process -Maintainers handle releases following semantic versioning: +Releases are **fully automated** using GitHub Actions: + +### Automated Release (Recommended) + +1. Merge PR to `main` with conventional commits +2. GitHub Actions automatically: + - Analyzes commits since last tag + - Determines version bump (major/minor/patch) + - Updates VERSION and CHANGELOG.md + - Creates git tag + - Creates GitHub release with changelog + +### Manual Release (Emergency Only) + +For hotfixes or manual releases: + +```bash +# Bump version (auto-detects from commits) +./scripts/bump-version.sh + +# Or specify bump type +./scripts/bump-version.sh major|minor|patch + +# Update changelog +./scripts/update-changelog.sh $(cat VERSION) + +# Commit and push +git add VERSION CHANGELOG.md +git commit -m "chore(release): bump version to $(cat VERSION) [skip ci]" +git tag -a "v$(cat VERSION)" -m "Release v$(cat VERSION)" +git push origin main --tags +``` -1. Update VERSION file -2. Update CHANGELOG.md -3. Create git tag: `git tag -a v0.x.0 -m "Release v0.x.0"` -4. Push tag: `git push origin v0.x.0` -5. Create GitHub release with notes +For detailed information about versioning and releases, see [docs/VERSIONING.md](docs/VERSIONING.md). ## Questions? diff --git a/Makefile b/Makefile index 97a3755..02e6f3b 100644 --- a/Makefile +++ b/Makefile @@ -67,7 +67,7 @@ lint: exit 1; \ } @echo "Running markdown lint..." - @npx --yes markdownlint-cli '**/*.md' --ignore node_modules --ignore .claude + @NODE_NO_WARNINGS=1 npx --yes markdownlint-cli '**/*.md' --ignore node_modules --ignore .claude # Auto-fix markdown lint issues lint-fix: @@ -77,7 +77,7 @@ lint-fix: exit 1; \ } @echo "Auto-fixing markdown lint issues..." - @npx --yes markdownlint-cli '**/*.md' --ignore node_modules --ignore .claude --fix + @NODE_NO_WARNINGS=1 npx --yes markdownlint-cli '**/*.md' --ignore node_modules --ignore .claude --fix # Install to user's oh-my-zsh install: diff --git a/README.md b/README.md index 49c9801..39d5b07 100644 --- a/README.md +++ b/README.md @@ -181,6 +181,7 @@ See full configuration options in the documentation (coming soon). ### Guides - [Development Guide](docs/DEVELOPMENT.md) - Local setup, testing, and contributing code +- [Versioning Guide](docs/VERSIONING.md) - Semantic versioning and release process - [Installation Guide](docs/INSTALLATION.md) - Coming in v0.2.0 - [Usage Guide](docs/USAGE.md) - Coming in v0.2.0 - [Configuration Reference](docs/CONFIGURATION.md) - Coming in v0.2.0 diff --git a/docs/VERSIONING.md b/docs/VERSIONING.md new file mode 100644 index 0000000..913ba30 --- /dev/null +++ b/docs/VERSIONING.md @@ -0,0 +1,268 @@ +# Semantic Versioning & Release Process + +This document describes how treehouse uses semantic versioning and automated releases. + +## Overview + +Treehouse follows [Semantic Versioning 2.0.0](https://semver.org/) and uses +[Conventional Commits](https://www.conventionalcommits.org/) to automate version bumping and changelog generation. + +## Semantic Versioning + +Given a version number `MAJOR.MINOR.PATCH`: + +- **MAJOR** - Incremented for incompatible API changes or breaking changes +- **MINOR** - Incremented for new features in a backwards-compatible manner +- **PATCH** - Incremented for backwards-compatible bug fixes + +## Conventional Commits + +All commits must follow the Conventional Commits specification: + +```text +type(dir): Description + +[optional body] + +[optional footer] +``` + +Where `type` is the commit type, `dir` is the component/directory/command name, and `Description` is a brief summary. + +### Commit Types + +- **feat** - New feature (triggers MINOR version bump) +- **fix** - Bug fix (triggers PATCH version bump) +- **perf** - Performance improvement (triggers PATCH version bump) +- **refactor** - Code refactoring (triggers PATCH version bump) +- **docs** - Documentation changes (no version bump) +- **style** - Code style changes (no version bump) +- **test** - Test changes (no version bump) +- **chore** - Maintenance tasks (no version bump) +- **ci** - CI configuration changes (no version bump) +- **build** - Build system changes (no version bump) +- **revert** - Revert a previous commit (no version bump) + +### Breaking Changes + +To indicate a breaking change, add `!` after the type/scope or include `BREAKING CHANGE:` in the footer: + +```text +feat(api)!: Remove deprecated authentication method + +BREAKING CHANGE: The old authentication method has been removed. +Use the new OAuth2 flow instead. +``` + +Breaking changes trigger a **MAJOR** version bump. + +### Examples + +```text +feat(lock): Add bulk lock operation for multiple worktrees +fix(status): Resolve issue with uncommitted changes detection +perf(list): Optimize worktree listing performance +docs(readme): Update installation instructions +refactor(clean): Simplify cleanup logic +chore(ci): Add automated release workflow +``` + +## Automated Release Process + +### How It Works + +1. **Developer Workflow** + - Create feature branch + - Make changes with conventional commits + - Create pull request to `main` + - CI validates commit messages + +2. **On Merge to Main** + - Release workflow analyzes commits since last tag + - Determines version bump type (major/minor/patch) + - Updates `VERSION` file + - Updates `CHANGELOG.md` with categorized changes + - Creates commit: `chore(release): bump version to X.Y.Z [skip ci]` + - Creates git tag: `vX.Y.Z` + - Creates GitHub release with changelog excerpt + +3. **Release Triggers** + - Automatic: On push to `main` with conventional commits + - Manual: Via GitHub Actions workflow dispatch + +### What Gets Released + +A release is created when commits include: + +- `feat:` - New features +- `fix:` - Bug fixes +- `perf:` - Performance improvements +- `refactor:` - Code refactoring + +Commits like `docs:`, `chore:`, `ci:` do NOT trigger releases by themselves. + +## Manual Versioning + +For local testing or manual releases, use the provided scripts: + +### Bump Version + +```bash +# Auto-detect version bump from commits +./scripts/bump-version.sh + +# Specify bump type manually +./scripts/bump-version.sh major +./scripts/bump-version.sh minor +./scripts/bump-version.sh patch +``` + +### Update Changelog + +```bash +# Update changelog for specific version +./scripts/update-changelog.sh 0.2.0 +``` + +## Version Files + +### VERSION + +Contains the current version number: + +```text +0.1.0 +``` + +### CHANGELOG.md + +Follows [Keep a Changelog](https://keepachangelog.com/) format: + +```markdown +## [Unreleased] + +## [0.2.0] - 2025-10-16 + +### Added + +- New feature descriptions + +### Fixed + +- Bug fix descriptions +``` + +## CI Workflows + +### Release Workflow (`.github/workflows/release.yml`) + +- **Triggers**: Push to `main`, manual dispatch +- **Purpose**: Create automated releases +- **Actions**: + - Analyzes commits + - Bumps version + - Updates changelog + - Creates tag and GitHub release + +### Commit Lint Workflow (`.github/workflows/commit-lint.yml`) + +- **Triggers**: Pull requests to `main`/`develop` +- **Purpose**: Validate commit messages +- **Actions**: + - Checks all PR commits + - Validates against conventional commits spec + - Fails if invalid commits found + +## Best Practices + +### For Contributors + +1. **Write Clear Commits** + - Use descriptive commit messages + - Follow `type(dir): Description` format + - Use component/directory/command as the scope + - Capitalize the description + - Reference issues when applicable + +2. **One Feature Per PR** + - Keep pull requests focused + - Makes release notes clearer + - Easier to review and revert if needed + +3. **Test Before Committing** + - Run `make test` locally + - Ensure all tests pass + - Validate syntax with `make quick` + +### For Maintainers + +1. **Review Commit Messages** + - Ensure PR commits follow conventions + - Request changes if needed + - Use "Squash and merge" with proper commit message + +2. **Manual Releases** + - Use workflow dispatch for hotfixes + - Specify bump type when needed + - Review changelog before releasing + +3. **Version Branches** + - `main` - Latest stable release + - `develop` - Development branch (if using git-flow) + - Feature branches - Individual features + +## Troubleshooting + +### Release Not Created + +If a release wasn't created after merging: + +1. Check if commits follow conventional format +2. Verify commits include `feat:`, `fix:`, `perf:`, or `refactor:` +3. Check GitHub Actions logs +4. Manually trigger release via workflow dispatch + +### Invalid Commit Messages + +If commit lint fails on PR: + +1. Review the failing commits +2. Amend or rebase to fix commit messages +3. Force push to update PR +4. Or use "Squash and merge" with proper message + +### Manual Release + +To create a release manually: + +```bash +# Bump version +./scripts/bump-version.sh minor + +# Update changelog +./scripts/update-changelog.sh $(cat VERSION) + +# Commit and tag +git add VERSION CHANGELOG.md +git commit -m "chore(release): bump version to $(cat VERSION)" +git tag -a "v$(cat VERSION)" -m "Release v$(cat VERSION)" + +# Push +git push origin main +git push origin "v$(cat VERSION)" +``` + +## References + +- [Semantic Versioning](https://semver.org/) +- [Conventional Commits](https://www.conventionalcommits.org/) +- [Keep a Changelog](https://keepachangelog.com/) +- [GitHub Actions Documentation](https://docs.github.com/en/actions) + +## Questions? + +If you have questions about versioning or releases: + +1. Check existing [GitHub Discussions](https://github.com/linnjs/treehouse/discussions) +2. Review closed issues with `release` label +3. Open a new discussion for guidance diff --git a/scripts/bump-version.sh b/scripts/bump-version.sh new file mode 100755 index 0000000..e61d0fd --- /dev/null +++ b/scripts/bump-version.sh @@ -0,0 +1,93 @@ +#!/usr/bin/env bash +# Script to bump version based on conventional commits +# Usage: ./scripts/bump-version.sh [major|minor|patch] + +set -euo pipefail + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Get current version from VERSION file +CURRENT_VERSION=$(cat VERSION | tr -d '[:space:]') + +if [[ ! "$CURRENT_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo -e "${RED}Error: Invalid version format in VERSION file: $CURRENT_VERSION${NC}" + exit 1 +fi + +# Parse version components +IFS='.' read -r MAJOR MINOR PATCH <<< "$CURRENT_VERSION" + +# Determine bump type +BUMP_TYPE="${1:-}" + +if [[ -z "$BUMP_TYPE" ]]; then + # Auto-detect from commits since last tag + LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "") + + if [[ -z "$LAST_TAG" ]]; then + echo -e "${YELLOW}No previous tag found, analyzing all commits...${NC}" + COMMITS=$(git log --pretty=format:"%s") + else + echo -e "${BLUE}Analyzing commits since $LAST_TAG...${NC}" + COMMITS=$(git log "$LAST_TAG"..HEAD --pretty=format:"%s") + fi + + # Check for breaking changes or major version indicators + if echo "$COMMITS" | grep -qE "^[a-z]+(\([a-z-]+\))?!:|BREAKING CHANGE:"; then + BUMP_TYPE="major" + echo -e "${YELLOW}Detected breaking changes${NC}" + # Check for new features + elif echo "$COMMITS" | grep -qE "^feat(\([a-z-]+\))?:"; then + BUMP_TYPE="minor" + echo -e "${BLUE}Detected new features${NC}" + # Otherwise it's a patch + elif echo "$COMMITS" | grep -qE "^fix(\([a-z-]+\))?:|^perf(\([a-z-]+\))?:|^refactor(\([a-z-]+\))?:"; then + BUMP_TYPE="patch" + echo -e "${GREEN}Detected fixes/patches${NC}" + else + echo -e "${YELLOW}No conventional commits found for versioning. Defaulting to patch.${NC}" + BUMP_TYPE="patch" + fi +fi + +# Calculate new version +case "$BUMP_TYPE" in + major) + MAJOR=$((MAJOR + 1)) + MINOR=0 + PATCH=0 + ;; + minor) + MINOR=$((MINOR + 1)) + PATCH=0 + ;; + patch) + PATCH=$((PATCH + 1)) + ;; + *) + echo -e "${RED}Error: Invalid bump type '$BUMP_TYPE'. Use: major, minor, or patch${NC}" + exit 1 + ;; +esac + +NEW_VERSION="$MAJOR.$MINOR.$PATCH" + +echo -e "${GREEN}Bumping version: $CURRENT_VERSION → $NEW_VERSION ($BUMP_TYPE)${NC}" + +# Update VERSION file +echo "$NEW_VERSION" > VERSION + +# Output for GitHub Actions +if [[ "${GITHUB_OUTPUT:-}" != "" ]]; then + echo "new_version=$NEW_VERSION" >> "$GITHUB_OUTPUT" + echo "old_version=$CURRENT_VERSION" >> "$GITHUB_OUTPUT" + echo "bump_type=$BUMP_TYPE" >> "$GITHUB_OUTPUT" +fi + +echo -e "${GREEN}✓ Version bumped successfully${NC}" +echo -e "${BLUE}New version: $NEW_VERSION${NC}" diff --git a/scripts/update-changelog.sh b/scripts/update-changelog.sh new file mode 100755 index 0000000..e30869a --- /dev/null +++ b/scripts/update-changelog.sh @@ -0,0 +1,172 @@ +#!/usr/bin/env bash +# Script to update CHANGELOG.md with new version +# Usage: ./scripts/update-changelog.sh + +set -euo pipefail + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +NEW_VERSION="${1:-}" +if [[ -z "$NEW_VERSION" ]]; then + echo -e "${RED}Error: Version argument required${NC}" + echo "Usage: $0 " + exit 1 +fi + +CHANGELOG_FILE="CHANGELOG.md" +DATE=$(date +%Y-%m-%d) + +echo -e "${BLUE}Updating CHANGELOG.md for version $NEW_VERSION...${NC}" + +# Get the last tag +LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "") + +if [[ -z "$LAST_TAG" ]]; then + echo -e "${YELLOW}No previous tag found, generating initial changelog entry...${NC}" + COMMITS=$(git log --pretty=format:"%s" --no-merges) +else + echo -e "${BLUE}Generating changelog since $LAST_TAG...${NC}" + COMMITS=$(git log "$LAST_TAG"..HEAD --pretty=format:"%s" --no-merges) +fi + +# Categorize commits +BREAKING_CHANGES="" +FEATURES="" +FIXES="" +PERFORMANCE="" +REFACTOR="" +DOCS="" +TESTS="" +CHORE="" +OTHER="" + +while IFS= read -r commit; do + # Skip empty commits + [[ -z "$commit" ]] && continue + + # Extract commit type and message + if [[ "$commit" =~ ^([a-z]+)(\([a-z0-9_-]+\))?(!)?: ]]; then + TYPE="${BASH_REMATCH[1]}" + BREAKING="${BASH_REMATCH[3]}" + MESSAGE="${commit#*: }" + + # Check for breaking change + if [[ "$BREAKING" == "!" ]] || [[ "$commit" =~ BREAKING[[:space:]]CHANGE ]]; then + BREAKING_CHANGES="${BREAKING_CHANGES}- ${MESSAGE}\n" + fi + + case "$TYPE" in + feat) + FEATURES="${FEATURES}- ${MESSAGE}\n" + ;; + fix) + FIXES="${FIXES}- ${MESSAGE}\n" + ;; + perf) + PERFORMANCE="${PERFORMANCE}- ${MESSAGE}\n" + ;; + refactor) + REFACTOR="${REFACTOR}- ${MESSAGE}\n" + ;; + docs) + DOCS="${DOCS}- ${MESSAGE}\n" + ;; + test) + TESTS="${TESTS}- ${MESSAGE}\n" + ;; + chore|ci|build) + CHORE="${CHORE}- ${MESSAGE}\n" + ;; + *) + OTHER="${OTHER}- ${MESSAGE}\n" + ;; + esac + else + # Non-conventional commit + OTHER="${OTHER}- ${commit}\n" + fi +done <<< "$COMMITS" + +# Build changelog entry +CHANGELOG_ENTRY="## [$NEW_VERSION] - $DATE\n" + +[[ -n "$BREAKING_CHANGES" ]] && CHANGELOG_ENTRY="${CHANGELOG_ENTRY}\n### ⚠️ BREAKING CHANGES\n\n${BREAKING_CHANGES}" +[[ -n "$FEATURES" ]] && CHANGELOG_ENTRY="${CHANGELOG_ENTRY}\n### Added\n\n${FEATURES}" +[[ -n "$FIXES" ]] && CHANGELOG_ENTRY="${CHANGELOG_ENTRY}\n### Fixed\n\n${FIXES}" +[[ -n "$PERFORMANCE" ]] && CHANGELOG_ENTRY="${CHANGELOG_ENTRY}\n### Performance\n\n${PERFORMANCE}" +[[ -n "$REFACTOR" ]] && CHANGELOG_ENTRY="${CHANGELOG_ENTRY}\n### Refactored\n\n${REFACTOR}" +[[ -n "$DOCS" ]] && CHANGELOG_ENTRY="${CHANGELOG_ENTRY}\n### Documentation\n\n${DOCS}" +[[ -n "$TESTS" ]] && CHANGELOG_ENTRY="${CHANGELOG_ENTRY}\n### Tests\n\n${TESTS}" +[[ -n "$CHORE" ]] && CHANGELOG_ENTRY="${CHANGELOG_ENTRY}\n### Maintenance\n\n${CHORE}" +[[ -n "$OTHER" ]] && CHANGELOG_ENTRY="${CHANGELOG_ENTRY}\n### Other\n\n${OTHER}" + +# Create temporary file +TEMP_FILE=$(mktemp) + +# Read the changelog and insert new version +IN_UNRELEASED=0 +UNRELEASED_WRITTEN=0 + +while IFS= read -r line; do + # Check if we're at the [Unreleased] section + if [[ "$line" =~ ^\[unreleased\]:|^\[Unreleased\]:|^##[[:space:]]*\[Unreleased\] ]]; then + IN_UNRELEASED=1 + fi + + # If we hit the first version section and haven't written the entry yet + if [[ "$line" =~ ^##[[:space:]]*\[[0-9]+\.[0-9]+\.[0-9]+\] ]] && [[ $UNRELEASED_WRITTEN -eq 0 ]]; then + # Write the new version entry before this line + echo -e "$CHANGELOG_ENTRY" >> "$TEMP_FILE" + UNRELEASED_WRITTEN=1 + fi + + # Skip the Unreleased section header if we're in it + if [[ $IN_UNRELEASED -eq 1 ]] && [[ "$line" =~ ^##[[:space:]]*\[Unreleased\] ]]; then + # Write fresh Unreleased section + echo "## [Unreleased]" >> "$TEMP_FILE" + echo "" >> "$TEMP_FILE" + IN_UNRELEASED=0 + continue + fi + + # Skip content in unreleased section (until next ## or [unreleased] link) + if [[ $IN_UNRELEASED -eq 1 ]]; then + if [[ "$line" =~ ^##[[:space:]] ]] || [[ "$line" =~ ^\[unreleased\]:|^\[Unreleased\]: ]]; then + IN_UNRELEASED=0 + else + continue + fi + fi + + # Update the [unreleased] comparison link at the bottom + if [[ "$line" =~ ^\[unreleased\]:[[:space:]]*(.*)/compare/(v[0-9]+\.[0-9]+\.[0-9]+)\.\.\.HEAD|^\[Unreleased\]:[[:space:]]*(.*)/compare/(v[0-9]+\.[0-9]+\.[0-9]+)\.\.\.HEAD ]]; then + REPO_URL="${BASH_REMATCH[1]}" + # Write updated unreleased link + echo "[unreleased]: ${REPO_URL}/compare/v${NEW_VERSION}...HEAD" >> "$TEMP_FILE" + # Write new version link + echo "[${NEW_VERSION}]: ${REPO_URL}/compare/${BASH_REMATCH[2]}...v${NEW_VERSION}" >> "$TEMP_FILE" + continue + fi + + # Write the line + echo "$line" >> "$TEMP_FILE" +done < "$CHANGELOG_FILE" + +# If we never found a version section, append at the end +if [[ $UNRELEASED_WRITTEN -eq 0 ]]; then + echo -e "\n$CHANGELOG_ENTRY" >> "$TEMP_FILE" +fi + +# Replace the original file +mv "$TEMP_FILE" "$CHANGELOG_FILE" + +echo -e "${GREEN}✓ CHANGELOG.md updated successfully${NC}" + +# Show a preview of what was added +echo -e "\n${BLUE}Changelog entry:${NC}" +echo -e "$CHANGELOG_ENTRY" | head -20 From 7614a10878c2514e1f46e286e0ff97dc43dfbf08 Mon Sep 17 00:00:00 2001 From: Justin Linn Date: Thu, 16 Oct 2025 05:19:23 -0400 Subject: [PATCH 2/5] chore(versioning): Add labels worflow and badge updates --- .github/labels.yml | 105 ++++++++++++++++++++ .github/workflows/pr-labeler.yml | 164 +++++++++++++++++++++++++++++++ .github/workflows/release.yml | 10 +- docs/VERSIONING.md | 36 +++++++ 4 files changed, 314 insertions(+), 1 deletion(-) create mode 100644 .github/labels.yml create mode 100644 .github/workflows/pr-labeler.yml diff --git a/.github/labels.yml b/.github/labels.yml new file mode 100644 index 0000000..ec2fff5 --- /dev/null +++ b/.github/labels.yml @@ -0,0 +1,105 @@ +# GitHub Labels Configuration +# These labels are used by the PR auto-labeler workflow +# +# To sync these labels with your repository, you can use: +# https://github.com/EndBug/label-sync +# or create them manually in GitHub UI + +# Type labels (based on conventional commits) +- name: enhancement + color: '84b6eb' + description: 'New feature or request (feat)' + +- name: bug + color: 'd73a4a' + description: 'Bug fix (fix)' + +- name: documentation + color: '0075ca' + description: 'Documentation updates (docs)' + +- name: style + color: 'e99695' + description: 'Code style changes (style)' + +- name: refactor + color: 'fbca04' + description: 'Code refactoring (refactor)' + +- name: performance + color: '5319e7' + description: 'Performance improvements (perf)' + +- name: testing + color: 'bfdadc' + description: 'Test updates (test)' + +- name: maintenance + color: 'fef2c0' + description: 'Maintenance tasks (chore)' + +- name: ci/cd + color: '128a0c' + description: 'CI/CD changes (ci)' + +- name: build + color: 'ededed' + description: 'Build system changes (build)' + +- name: revert + color: 'ffffff' + description: 'Revert changes (revert)' + +- name: breaking change + color: 'd93f0b' + description: 'Breaking changes (major version bump)' + +# Size labels (auto-added based on PR size) +- name: size/XS + color: '00ff00' + description: 'Extra small PR (< 10 lines)' + +- name: size/S + color: '77dd77' + description: 'Small PR (< 50 lines)' + +- name: size/M + color: 'ffff00' + description: 'Medium PR (< 200 lines)' + +- name: size/L + color: 'ff9900' + description: 'Large PR (< 500 lines)' + +- name: size/XL + color: 'ff0000' + description: 'Extra large PR (>= 500 lines)' + +# Additional useful labels +- name: dependencies + color: '0366d6' + description: 'Dependency updates' + +- name: good first issue + color: '7057ff' + description: 'Good for newcomers' + +- name: help wanted + color: '008672' + description: 'Extra attention is needed' + +- name: question + color: 'd876e3' + description: 'Further information is requested' + +- name: wontfix + color: 'ffffff' + description: "This will not be worked on" + +- name: duplicate + color: 'cfd3d7' + description: 'This issue or pull request already exists' + +- name: invalid + color: 'e4e669' + description: 'Something is invalid' diff --git a/.github/workflows/pr-labeler.yml b/.github/workflows/pr-labeler.yml new file mode 100644 index 0000000..c6835bb --- /dev/null +++ b/.github/workflows/pr-labeler.yml @@ -0,0 +1,164 @@ +name: PR Auto-Labeler + +on: + pull_request: + types: [opened, synchronize, reopened] + +permissions: + contents: read + pull-requests: write + issues: write + +jobs: + label: + name: Auto-label PR + runs-on: ubuntu-latest + + steps: + - name: Harden Runner + uses: step-security/harden-runner@v2 + with: + egress-policy: audit + + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + ref: ${{ github.event.pull_request.head.sha }} + + - name: Analyze commits and add labels + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const { data: commits } = await github.rest.pulls.listCommits({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: context.issue.number, + }); + + // Extract commit messages + const messages = commits.map(c => c.commit.message); + + // Track which types are present + const types = new Set(); + + // Analyze commit types + for (const msg of messages) { + const match = msg.match(/^(feat|fix|docs|style|refactor|perf|test|chore|ci|build|revert)(\([a-z0-9_-]+\))?(!)?: /); + if (match) { + types.add(match[1]); + // Check for breaking change + if (match[3] === '!' || msg.includes('BREAKING CHANGE:')) { + types.add('breaking'); + } + } + } + + // Map commit types to labels + const labelMap = { + 'feat': 'enhancement', + 'fix': 'bug', + 'docs': 'documentation', + 'style': 'style', + 'refactor': 'refactor', + 'perf': 'performance', + 'test': 'testing', + 'chore': 'maintenance', + 'ci': 'ci/cd', + 'build': 'build', + 'revert': 'revert', + 'breaking': 'breaking change' + }; + + // Collect labels to add + const labelsToAdd = []; + for (const type of types) { + if (labelMap[type]) { + labelsToAdd.push(labelMap[type]); + } + } + + // Remove duplicates + const uniqueLabels = [...new Set(labelsToAdd)]; + + if (uniqueLabels.length > 0) { + console.log(`Adding labels: ${uniqueLabels.join(', ')}`); + + // Add labels to PR + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + labels: uniqueLabels + }); + + // Add comment with summary + const typesList = Array.from(types).filter(t => t !== 'breaking').join(', '); + const breakingNote = types.has('breaking') ? '\n\n⚠️ **This PR contains breaking changes!**' : ''; + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: `🏷️ Auto-labeled based on commits: \`${typesList}\`${breakingNote}` + }); + } else { + console.log('No conventional commit types found, skipping labeling'); + } + + - name: Add size label + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const { data: pr } = await github.rest.pulls.get({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: context.issue.number, + }); + + const additions = pr.additions; + const deletions = pr.deletions; + const total = additions + deletions; + + let sizeLabel = ''; + if (total < 10) { + sizeLabel = 'size/XS'; + } else if (total < 50) { + sizeLabel = 'size/S'; + } else if (total < 200) { + sizeLabel = 'size/M'; + } else if (total < 500) { + sizeLabel = 'size/L'; + } else { + sizeLabel = 'size/XL'; + } + + console.log(`PR size: ${total} lines (${additions} additions, ${deletions} deletions) → ${sizeLabel}`); + + // Remove old size labels + const { data: currentLabels } = await github.rest.issues.listLabelsOnIssue({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + }); + + for (const label of currentLabels) { + if (label.name.startsWith('size/')) { + await github.rest.issues.removeLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + name: label.name, + }).catch(() => {}); + } + } + + // Add new size label + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + labels: [sizeLabel] + }); diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f4ae9aa..41988b2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -98,6 +98,14 @@ jobs: chmod +x scripts/update-changelog.sh ./scripts/update-changelog.sh "${{ steps.bump_version.outputs.new_version }}" + - name: Update README badge + if: steps.check_release.outputs.release_needed == 'true' + run: | + VERSION="${{ steps.bump_version.outputs.new_version }}" + # Update version badge in README.md + sed -i "s/version-[0-9]\+\.[0-9]\+\.[0-9]\+-blue/version-${VERSION}-blue/" README.md + echo "✓ Updated version badge to ${VERSION}" + - name: Extract release notes if: steps.check_release.outputs.release_needed == 'true' id: extract_notes @@ -121,7 +129,7 @@ jobs: - name: Commit version bump if: steps.check_release.outputs.release_needed == 'true' run: | - git add VERSION CHANGELOG.md + git add VERSION CHANGELOG.md README.md git commit -m "chore(release): bump version to ${{ steps.bump_version.outputs.new_version }} [skip ci]" git push origin main diff --git a/docs/VERSIONING.md b/docs/VERSIONING.md index 913ba30..fc0307c 100644 --- a/docs/VERSIONING.md +++ b/docs/VERSIONING.md @@ -82,6 +82,7 @@ chore(ci): Add automated release workflow - Determines version bump type (major/minor/patch) - Updates `VERSION` file - Updates `CHANGELOG.md` with categorized changes + - Updates `README.md` version badge automatically - Creates commit: `chore(release): bump version to X.Y.Z [skip ci]` - Creates git tag: `vX.Y.Z` - Creates GitHub release with changelog excerpt @@ -162,6 +163,7 @@ Follows [Keep a Changelog](https://keepachangelog.com/) format: - Analyzes commits - Bumps version - Updates changelog + - Updates README version badge - Creates tag and GitHub release ### Commit Lint Workflow (`.github/workflows/commit-lint.yml`) @@ -173,6 +175,40 @@ Follows [Keep a Changelog](https://keepachangelog.com/) format: - Validates against conventional commits spec - Fails if invalid commits found +### PR Auto-Labeler Workflow (`.github/workflows/pr-labeler.yml`) + +- **Triggers**: Pull requests opened, synchronized, or reopened +- **Purpose**: Automatically label PRs based on content +- **Actions**: + - Analyzes commit types in PR + - Adds type labels (enhancement, bug, documentation, etc.) + - Adds size label based on lines changed (XS/S/M/L/XL) + - Adds breaking change label if detected + - Posts summary comment on PR + +#### Labels Added + +**Type Labels** (based on conventional commits): + +- `enhancement` - New features (feat) +- `bug` - Bug fixes (fix) +- `documentation` - Documentation updates (docs) +- `style` - Code style changes +- `refactor` - Code refactoring +- `performance` - Performance improvements (perf) +- `testing` - Test updates (test) +- `maintenance` - Maintenance tasks (chore) +- `ci/cd` - CI/CD changes +- `breaking change` - Breaking changes (major version) + +**Size Labels** (based on lines changed): + +- `size/XS` - < 10 lines +- `size/S` - < 50 lines +- `size/M` - < 200 lines +- `size/L` - < 500 lines +- `size/XL` - >= 500 lines + ## Best Practices ### For Contributors From 135e7590437730c5545b6349d835788392c2bd1c Mon Sep 17 00:00:00 2001 From: Justin Linn Date: Thu, 16 Oct 2025 05:24:40 -0400 Subject: [PATCH 3/5] chore(versioning): Fix dependabot and add label descriptions --- .github/dependabot.yml | 6 +- .github/labels.yml | 126 +++++++++++++++++++++------------- .github/workflows/release.yml | 2 +- 3 files changed, 85 insertions(+), 49 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 8f2fc35..1f44c91 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -5,6 +5,10 @@ updates: directory: "/" schedule: interval: "monthly" + open-pull-requests-limit: 5 labels: - "dependencies" - - "github-actions" + - "ci/cd" + commit-message: + prefix: "chore(deps)" + include: "scope" diff --git a/.github/labels.yml b/.github/labels.yml index ec2fff5..897cb2f 100644 --- a/.github/labels.yml +++ b/.github/labels.yml @@ -4,102 +4,134 @@ # To sync these labels with your repository, you can use: # https://github.com/EndBug/label-sync # or create them manually in GitHub UI +# +# Color scheme: +# - Blue/Purple: New features and enhancements +# - Red/Orange: Bugs and breaking changes +# - Teal/Cyan: Documentation and testing +# - Yellow: Refactoring and caution +# - Green: CI/CD and automation +# - Gray: Infrastructure and builds # Type labels (based on conventional commits) - name: enhancement - color: '84b6eb' - description: 'New feature or request (feat)' + color: '7B68EE' + description: '✨ New feature or enhancement - adds functionality (conventional commit: feat)' - name: bug - color: 'd73a4a' - description: 'Bug fix (fix)' + color: 'DC143C' + description: '🐛 Bug fix - resolves an issue or error (conventional commit: fix)' - name: documentation - color: '0075ca' - description: 'Documentation updates (docs)' + color: '20B2AA' + description: '📚 Documentation improvements - updates docs, comments, or guides (conventional commit: docs)' - name: style - color: 'e99695' - description: 'Code style changes (style)' + color: 'FF69B4' + description: '💅 Code style changes - formatting, whitespace, missing semicolons (conventional commit: style)' - name: refactor - color: 'fbca04' - description: 'Code refactoring (refactor)' + color: 'FFD700' + description: '♻️ Code refactoring - restructuring without changing behavior (conventional commit: refactor)' - name: performance - color: '5319e7' - description: 'Performance improvements (perf)' + color: '9370DB' + description: '⚡ Performance improvements - optimizations and speed enhancements (conventional commit: perf)' - name: testing - color: 'bfdadc' - description: 'Test updates (test)' + color: '48D1CC' + description: '🧪 Test updates - adding or updating tests (conventional commit: test)' - name: maintenance - color: 'fef2c0' - description: 'Maintenance tasks (chore)' + color: 'D2B48C' + description: '🔧 Maintenance tasks - routine upkeep and housekeeping (conventional commit: chore)' - name: ci/cd - color: '128a0c' - description: 'CI/CD changes (ci)' + color: '228B22' + description: '🚀 CI/CD changes - workflow automation and deployment (conventional commit: ci)' - name: build - color: 'ededed' - description: 'Build system changes (build)' + color: '808080' + description: '🏗️ Build system changes - build tools, dependencies, config (conventional commit: build)' - name: revert - color: 'ffffff' - description: 'Revert changes (revert)' + color: '696969' + description: '⏪ Revert changes - undoing previous commits (conventional commit: revert)' - name: breaking change - color: 'd93f0b' - description: 'Breaking changes (major version bump)' + color: 'FF4500' + description: '💥 BREAKING CHANGE - incompatible API changes requiring major version bump' # Size labels (auto-added based on PR size) - name: size/XS - color: '00ff00' - description: 'Extra small PR (< 10 lines)' + color: '00E676' + description: '📏 Extra small change - less than 10 lines modified' - name: size/S - color: '77dd77' - description: 'Small PR (< 50 lines)' + color: '76FF03' + description: '📏 Small change - less than 50 lines modified' - name: size/M - color: 'ffff00' - description: 'Medium PR (< 200 lines)' + color: 'FDD835' + description: '📏 Medium change - less than 200 lines modified' - name: size/L - color: 'ff9900' - description: 'Large PR (< 500 lines)' + color: 'FF6D00' + description: '📏 Large change - less than 500 lines modified' - name: size/XL - color: 'ff0000' - description: 'Extra large PR (>= 500 lines)' + color: 'D50000' + description: '📏 Extra large change - 500+ lines modified, consider splitting' # Additional useful labels - name: dependencies - color: '0366d6' - description: 'Dependency updates' + color: '0366D6' + description: '📦 Dependency updates - library and package upgrades' + +- name: security + color: 'B22222' + description: '🔒 Security fixes - patches for vulnerabilities' - name: good first issue - color: '7057ff' - description: 'Good for newcomers' + color: '7057FF' + description: '👋 Good for newcomers - great starting point for new contributors' - name: help wanted color: '008672' - description: 'Extra attention is needed' + description: '🙏 Help wanted - seeking community input or assistance' + +- name: priority: high + color: 'FF0000' + description: '🔴 High priority - needs immediate attention' + +- name: priority: medium + color: 'FFA500' + description: '🟡 Medium priority - should be addressed soon' + +- name: priority: low + color: '00FF00' + description: '🟢 Low priority - can be deferred' - name: question - color: 'd876e3' - description: 'Further information is requested' + color: 'BA68C8' + description: '❓ Question - seeking clarification or discussion' - name: wontfix - color: 'ffffff' - description: "This will not be worked on" + color: 'E0E0E0' + description: '⛔ Won\'t fix - this will not be worked on' - name: duplicate - color: 'cfd3d7' - description: 'This issue or pull request already exists' + color: '9E9E9E' + description: '🔄 Duplicate - this issue or PR already exists elsewhere' - name: invalid - color: 'e4e669' - description: 'Something is invalid' + color: 'FFEB3B' + description: '❌ Invalid - not applicable or incorrect' + +- name: stale + color: 'BDBDBD' + description: '⏳ Stale - no recent activity, may be closed' + +- name: blocked + color: 'B71C1C' + description: '🚫 Blocked - waiting on external dependency or decision' diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 41988b2..6587df3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -35,7 +35,7 @@ jobs: name: Create Release runs-on: ubuntu-latest # Only run if not a bot commit (to avoid release loops) - if: "!contains(github.event.head_commit.message, '[skip ci]') && !contains(github.event.head_commit.message, 'chore(release)')" + if: ${{ !contains(github.event.head_commit.message, '[skip ci]') && !contains(github.event.head_commit.message, 'chore(release)') }} steps: - name: Harden Runner From 29aa138aab783880dd876c9217c96d6bfc685b37 Mon Sep 17 00:00:00 2001 From: Justin Linn Date: Thu, 16 Oct 2025 05:34:48 -0400 Subject: [PATCH 4/5] chore(versioning): Add aditional labels and sync --- .github/labels.yml | 75 +++++++++++-------- .github/workflows/pr-labeler.yml | 74 ++++++++++++++++--- scripts/sync-labels.sh | 123 +++++++++++++++++++++++++++++++ 3 files changed, 230 insertions(+), 42 deletions(-) create mode 100755 scripts/sync-labels.sh diff --git a/.github/labels.yml b/.github/labels.yml index 897cb2f..110ff0c 100644 --- a/.github/labels.yml +++ b/.github/labels.yml @@ -15,123 +15,136 @@ # Type labels (based on conventional commits) - name: enhancement - color: '7B68EE' - description: '✨ New feature or enhancement - adds functionality (conventional commit: feat)' + color: 'A28EFF' + description: '✨ Enhancement - improves existing functionality' - name: bug - color: 'DC143C' + color: 'FF1744' description: '🐛 Bug fix - resolves an issue or error (conventional commit: fix)' - name: documentation - color: '20B2AA' + color: '00E5FF' description: '📚 Documentation improvements - updates docs, comments, or guides (conventional commit: docs)' - name: style - color: 'FF69B4' + color: 'FF4081' description: '💅 Code style changes - formatting, whitespace, missing semicolons (conventional commit: style)' - name: refactor - color: 'FFD700' + color: 'FFEA00' description: '♻️ Code refactoring - restructuring without changing behavior (conventional commit: refactor)' - name: performance - color: '9370DB' + color: 'AA00FF' description: '⚡ Performance improvements - optimizations and speed enhancements (conventional commit: perf)' - name: testing - color: '48D1CC' + color: '00E676' description: '🧪 Test updates - adding or updating tests (conventional commit: test)' - name: maintenance - color: 'D2B48C' + color: 'FFB300' description: '🔧 Maintenance tasks - routine upkeep and housekeeping (conventional commit: chore)' - name: ci/cd - color: '228B22' + color: '00C853' description: '🚀 CI/CD changes - workflow automation and deployment (conventional commit: ci)' - name: build - color: '808080' + color: '9E9E9E' description: '🏗️ Build system changes - build tools, dependencies, config (conventional commit: build)' - name: revert - color: '696969' + color: '78909C' description: '⏪ Revert changes - undoing previous commits (conventional commit: revert)' - name: breaking change - color: 'FF4500' + color: 'FF3D00' description: '💥 BREAKING CHANGE - incompatible API changes requiring major version bump' # Size labels (auto-added based on PR size) - name: size/XS color: '00E676' - description: '📏 Extra small change - less than 10 lines modified' + description: '🐭 Extra small change - less than 10 lines modified' - name: size/S color: '76FF03' - description: '📏 Small change - less than 50 lines modified' + description: '🐿️ Small change - less than 50 lines modified' - name: size/M color: 'FDD835' - description: '📏 Medium change - less than 200 lines modified' + description: '🐕 Medium change - less than 200 lines modified' - name: size/L color: 'FF6D00' - description: '📏 Large change - less than 500 lines modified' + description: '🐘 Large change - less than 500 lines modified' - name: size/XL color: 'D50000' - description: '📏 Extra large change - 500+ lines modified, consider splitting' + description: '🦖 Extra large change - 500+ lines modified, consider splitting' # Additional useful labels - name: dependencies - color: '0366D6' + color: '2196F3' description: '📦 Dependency updates - library and package upgrades' - name: security - color: 'B22222' + color: 'D50000' description: '🔒 Security fixes - patches for vulnerabilities' - name: good first issue - color: '7057FF' + color: '7C4DFF' description: '👋 Good for newcomers - great starting point for new contributors' - name: help wanted - color: '008672' + color: '00BFA5' description: '🙏 Help wanted - seeking community input or assistance' - name: priority: high - color: 'FF0000' + color: 'FF1744' description: '🔴 High priority - needs immediate attention' - name: priority: medium - color: 'FFA500' + color: 'FF9100' description: '🟡 Medium priority - should be addressed soon' - name: priority: low - color: '00FF00' + color: '69F0AE' description: '🟢 Low priority - can be deferred' - name: question - color: 'BA68C8' + color: 'E040FB' description: '❓ Question - seeking clarification or discussion' - name: wontfix - color: 'E0E0E0' + color: 'CFD8DC' description: '⛔ Won\'t fix - this will not be worked on' - name: duplicate - color: '9E9E9E' + color: 'B0BEC5' description: '🔄 Duplicate - this issue or PR already exists elsewhere' - name: invalid - color: 'FFEB3B' + color: 'FDD835' description: '❌ Invalid - not applicable or incorrect' - name: stale - color: 'BDBDBD' + color: 'EEEEEE' description: '⏳ Stale - no recent activity, may be closed' - name: blocked - color: 'B71C1C' + color: 'C62828' description: '🚫 Blocked - waiting on external dependency or decision' + +# Additional workflow labels +- name: chore + color: '00B8D4' + description: '🧹 Repository chore or maintenance work' + +- name: feature + color: 'FF6EC7' + description: '🌟 New feature - adds brand new functionality (conventional commit: feat)' + +- name: hacktoberfest-accepted + color: 'FF8500' + description: '🎃 Hacktoberfest accepted - auto-applied in October for contributors' diff --git a/.github/workflows/pr-labeler.yml b/.github/workflows/pr-labeler.yml index c6835bb..d3482d4 100644 --- a/.github/workflows/pr-labeler.yml +++ b/.github/workflows/pr-labeler.yml @@ -57,14 +57,14 @@ jobs: // Map commit types to labels const labelMap = { - 'feat': 'enhancement', + 'feat': 'feature', 'fix': 'bug', 'docs': 'documentation', 'style': 'style', 'refactor': 'refactor', 'perf': 'performance', 'test': 'testing', - 'chore': 'maintenance', + 'chore': 'chore', 'ci': 'ci/cd', 'build': 'build', 'revert': 'revert', @@ -93,16 +93,20 @@ jobs: labels: uniqueLabels }); - // Add comment with summary - const typesList = Array.from(types).filter(t => t !== 'breaking').join(', '); - const breakingNote = types.has('breaking') ? '\n\n⚠️ **This PR contains breaking changes!**' : ''; + // Only comment when PR is first opened (not on every update) + if (context.payload.action === 'opened') { + const typesList = Array.from(types).filter(t => t !== 'breaking').join(', '); + const breakingNote = types.has('breaking') ? '\n\n⚠️ **This PR contains breaking changes!**' : ''; - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - body: `🏷️ Auto-labeled based on commits: \`${typesList}\`${breakingNote}` - }); + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: `🏷️ Auto-labeled based on commits: \`${typesList}\`${breakingNote}` + }); + } else { + console.log('Labels updated (no comment on synchronize/reopened to reduce noise)'); + } } else { console.log('No conventional commit types found, skipping labeling'); } @@ -162,3 +166,51 @@ jobs: issue_number: context.issue.number, labels: [sizeLabel] }); + + - name: Hacktoberfest auto-accept + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + // Only run during October + const now = new Date(); + const month = now.getMonth(); // 0 = January, 9 = October + + if (month !== 9) { + console.log('Not October - skipping Hacktoberfest labeling'); + return; + } + + // Check if PR author is a previous contributor + const author = context.payload.pull_request.user.login; + + // Get all merged PRs from this author + const { data: searchResults } = await github.rest.search.issuesAndPullRequests({ + q: `repo:${context.repo.owner}/${context.repo.repo} author:${author} type:pr is:merged`, + per_page: 1 + }); + + const hasPreviousContributions = searchResults.total_count > 0; + + if (hasPreviousContributions) { + console.log(`${author} is a previous contributor - adding hacktoberfest-accepted label`); + + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + labels: ['hacktoberfest-accepted'] + }); + + // Only comment on first opened + if (context.payload.action === 'opened') { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: '🎃 **Happy Hacktoberfest!** Thank you for being a returning contributor. Your PR has been automatically accepted for Hacktoberfest.' + }); + } + } else { + console.log(`${author} is a new contributor - manual review required for Hacktoberfest`); + } diff --git a/scripts/sync-labels.sh b/scripts/sync-labels.sh new file mode 100755 index 0000000..6eaaf7f --- /dev/null +++ b/scripts/sync-labels.sh @@ -0,0 +1,123 @@ +#!/usr/bin/env bash +# Script to sync GitHub labels from .github/labels.yml +# Usage: ./scripts/sync-labels.sh + +set -euo pipefail + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +LABELS_FILE=".github/labels.yml" + +# Check if gh CLI is installed +if ! command -v gh &> /dev/null; then + echo -e "${RED}Error: GitHub CLI (gh) is not installed${NC}" + echo "Install with: brew install gh (macOS) or see https://cli.github.com" + exit 1 +fi + +# Check if logged in +if ! gh auth status &> /dev/null; then + echo -e "${RED}Error: Not logged in to GitHub CLI${NC}" + echo "Run: gh auth login" + exit 1 +fi + +# Check if labels file exists +if [[ ! -f "$LABELS_FILE" ]]; then + echo -e "${RED}Error: $LABELS_FILE not found${NC}" + exit 1 +fi + +echo -e "${BLUE}🏷️ Syncing GitHub labels from $LABELS_FILE${NC}" +echo "" + +# Parse YAML and create/update labels +# This uses a simple approach - you could also use yq for more robust parsing +LABEL_NAME="" +LABEL_COLOR="" +LABEL_DESC="" + +while IFS= read -r line; do + # Skip comments and empty lines + [[ "$line" =~ ^[[:space:]]*# ]] && continue + [[ -z "${line// }" ]] && continue + + # Parse name + if [[ "$line" =~ ^-[[:space:]]*name:[[:space:]]*(.+)$ ]]; then + # If we have a complete label, process it + if [[ -n "$LABEL_NAME" ]]; then + echo -e "${YELLOW}Processing: $LABEL_NAME${NC}" + + # Check if label exists + if gh api "repos/:owner/:repo/labels/$LABEL_NAME" &> /dev/null; then + # Update existing label + gh api --method PATCH "repos/:owner/:repo/labels/$LABEL_NAME" \ + -f color="$LABEL_COLOR" \ + -f description="$LABEL_DESC" \ + > /dev/null && \ + echo -e "${GREEN} ✓ Updated${NC}" || \ + echo -e "${RED} ✗ Failed to update${NC}" + else + # Create new label + gh api --method POST "repos/:owner/:repo/labels" \ + -f name="$LABEL_NAME" \ + -f color="$LABEL_COLOR" \ + -f description="$LABEL_DESC" \ + > /dev/null && \ + echo -e "${GREEN} ✓ Created${NC}" || \ + echo -e "${RED} ✗ Failed to create${NC}" + fi + fi + + # Extract new label name + LABEL_NAME="${BASH_REMATCH[1]}" + LABEL_NAME="${LABEL_NAME#"${LABEL_NAME%%[![:space:]]*}"}" # Trim leading + LABEL_NAME="${LABEL_NAME%"${LABEL_NAME##*[![:space:]]}"}" # Trim trailing + LABEL_COLOR="" + LABEL_DESC="" + fi + + # Parse color + if [[ "$line" =~ ^[[:space:]]+color:[[:space:]]*[\'\"]*([A-Fa-f0-9]+)[\'\"]*$ ]]; then + LABEL_COLOR="${BASH_REMATCH[1]}" + fi + + # Parse description + if [[ "$line" =~ ^[[:space:]]+description:[[:space:]]*[\'\"]*(.+)[\'\"]*$ ]]; then + LABEL_DESC="${BASH_REMATCH[1]}" + # Remove leading/trailing quotes + LABEL_DESC="${LABEL_DESC#[\'\"]}" + LABEL_DESC="${LABEL_DESC%[\'\"]}" + fi +done < "$LABELS_FILE" + +# Process the last label +if [[ -n "$LABEL_NAME" ]]; then + echo -e "${YELLOW}Processing: $LABEL_NAME${NC}" + + if gh api "repos/:owner/:repo/labels/$LABEL_NAME" &> /dev/null; then + gh api --method PATCH "repos/:owner/:repo/labels/$LABEL_NAME" \ + -f color="$LABEL_COLOR" \ + -f description="$LABEL_DESC" \ + > /dev/null && \ + echo -e "${GREEN} ✓ Updated${NC}" || \ + echo -e "${RED} ✗ Failed to update${NC}" + else + gh api --method POST "repos/:owner/:repo/labels" \ + -f name="$LABEL_NAME" \ + -f color="$LABEL_COLOR" \ + -f description="$LABEL_DESC" \ + > /dev/null && \ + echo -e "${GREEN} ✓ Created${NC}" || \ + echo -e "${RED} ✗ Failed to create${NC}" + fi +fi + +echo "" +echo -e "${GREEN}✓ Label sync complete!${NC}" +echo -e "${BLUE}View labels at: https://github.com/$(gh repo view --json nameWithOwner -q .nameWithOwner)/labels${NC}" From e4156346cd5d2c7be96afbda703a439747a8a6d8 Mon Sep 17 00:00:00 2001 From: Justin Linn Date: Thu, 16 Oct 2025 05:40:43 -0400 Subject: [PATCH 5/5] chore(versioning): Check for labels to prevent re-labeling --- .github/workflows/pr-labeler.yml | 137 +++++++++++++++++++++++-------- 1 file changed, 103 insertions(+), 34 deletions(-) diff --git a/.github/workflows/pr-labeler.yml b/.github/workflows/pr-labeler.yml index d3482d4..60c8160 100644 --- a/.github/workflows/pr-labeler.yml +++ b/.github/workflows/pr-labeler.yml @@ -83,27 +83,58 @@ jobs: const uniqueLabels = [...new Set(labelsToAdd)]; if (uniqueLabels.length > 0) { - console.log(`Adding labels: ${uniqueLabels.join(', ')}`); - - // Add labels to PR - await github.rest.issues.addLabels({ + // Get current labels to avoid re-adding + const { data: currentLabels } = await github.rest.issues.listLabelsOnIssue({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, - labels: uniqueLabels }); + const currentLabelNames = new Set(currentLabels.map(l => l.name)); + const labelsToActuallyAdd = uniqueLabels.filter(label => !currentLabelNames.has(label)); + + if (labelsToActuallyAdd.length > 0) { + console.log(`Adding new labels: ${labelsToActuallyAdd.join(', ')}`); + + // Add labels to PR + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + labels: labelsToActuallyAdd + }); + } else { + console.log('All labels already present, no changes needed'); + } + // Only comment when PR is first opened (not on every update) if (context.payload.action === 'opened') { - const typesList = Array.from(types).filter(t => t !== 'breaking').join(', '); - const breakingNote = types.has('breaking') ? '\n\n⚠️ **This PR contains breaking changes!**' : ''; - - await github.rest.issues.createComment({ + // Check if we've already commented + const { data: comments } = await github.rest.issues.listComments({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, - body: `🏷️ Auto-labeled based on commits: \`${typesList}\`${breakingNote}` }); + + const botCommentExists = comments.some(comment => + comment.user.type === 'Bot' && + comment.body.includes('🏷️ Auto-labeled based on commits:') + ); + + if (!botCommentExists) { + const typesList = Array.from(types).filter(t => t !== 'breaking').join(', '); + const breakingNote = types.has('breaking') ? '\n\n⚠️ **This PR contains breaking changes!**' : ''; + + console.log('Posting auto-label summary comment'); + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: `🏷️ Auto-labeled based on commits: \`${typesList}\`${breakingNote}` + }); + } else { + console.log('Auto-label comment already exists, skipping'); + } } else { console.log('Labels updated (no comment on synchronize/reopened to reduce noise)'); } @@ -141,31 +172,40 @@ jobs: console.log(`PR size: ${total} lines (${additions} additions, ${deletions} deletions) → ${sizeLabel}`); - // Remove old size labels + // Get current labels const { data: currentLabels } = await github.rest.issues.listLabelsOnIssue({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, }); - for (const label of currentLabels) { - if (label.name.startsWith('size/')) { - await github.rest.issues.removeLabel({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - name: label.name, - }).catch(() => {}); + const currentSizeLabel = currentLabels.find(l => l.name.startsWith('size/'))?.name; + + // Only update if size label changed + if (currentSizeLabel === sizeLabel) { + console.log(`Size label ${sizeLabel} already correct, no change needed`); + } else { + // Remove old size labels + for (const label of currentLabels) { + if (label.name.startsWith('size/')) { + await github.rest.issues.removeLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + name: label.name, + }).catch(() => {}); + } } - } - // Add new size label - await github.rest.issues.addLabels({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - labels: [sizeLabel] - }); + // Add new size label + console.log(`Updating size label: ${currentSizeLabel || 'none'} → ${sizeLabel}`); + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + labels: [sizeLabel] + }); + } - name: Hacktoberfest auto-accept uses: actions/github-script@v7 @@ -193,23 +233,52 @@ jobs: const hasPreviousContributions = searchResults.total_count > 0; if (hasPreviousContributions) { - console.log(`${author} is a previous contributor - adding hacktoberfest-accepted label`); + console.log(`${author} is a previous contributor`); - await github.rest.issues.addLabels({ + // Check if label already exists + const { data: currentLabels } = await github.rest.issues.listLabelsOnIssue({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, - labels: ['hacktoberfest-accepted'] }); - // Only comment on first opened - if (context.payload.action === 'opened') { - await github.rest.issues.createComment({ + const hasHacktoberfestLabel = currentLabels.some(l => l.name === 'hacktoberfest-accepted'); + + if (!hasHacktoberfestLabel) { + console.log('Adding hacktoberfest-accepted label'); + + await github.rest.issues.addLabels({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, - body: '🎃 **Happy Hacktoberfest!** Thank you for being a returning contributor. Your PR has been automatically accepted for Hacktoberfest.' + labels: ['hacktoberfest-accepted'] }); + + // Check if we've already commented about Hacktoberfest + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + }); + + const hacktoberfestCommentExists = comments.some(comment => + comment.user.type === 'Bot' && + comment.body.includes('Happy Hacktoberfest!') + ); + + if (!hacktoberfestCommentExists && context.payload.action === 'opened') { + console.log('Posting Hacktoberfest welcome comment'); + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: '🎃 **Happy Hacktoberfest!** Thank you for being a returning contributor. Your PR has been automatically accepted for Hacktoberfest.' + }); + } else { + console.log('Hacktoberfest comment already exists or not first opened, skipping'); + } + } else { + console.log('Hacktoberfest label already present'); } } else { console.log(`${author} is a new contributor - manual review required for Hacktoberfest`);