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 new file mode 100644 index 0000000..110ff0c --- /dev/null +++ b/.github/labels.yml @@ -0,0 +1,150 @@ +# 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 +# +# 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: 'A28EFF' + description: 'โœจ Enhancement - improves existing functionality' + +- name: bug + color: 'FF1744' + description: '๐Ÿ› Bug fix - resolves an issue or error (conventional commit: fix)' + +- name: documentation + color: '00E5FF' + description: '๐Ÿ“š Documentation improvements - updates docs, comments, or guides (conventional commit: docs)' + +- name: style + color: 'FF4081' + description: '๐Ÿ’… Code style changes - formatting, whitespace, missing semicolons (conventional commit: style)' + +- name: refactor + color: 'FFEA00' + description: 'โ™ป๏ธ Code refactoring - restructuring without changing behavior (conventional commit: refactor)' + +- name: performance + color: 'AA00FF' + description: 'โšก Performance improvements - optimizations and speed enhancements (conventional commit: perf)' + +- name: testing + color: '00E676' + description: '๐Ÿงช Test updates - adding or updating tests (conventional commit: test)' + +- name: maintenance + color: 'FFB300' + description: '๐Ÿ”ง Maintenance tasks - routine upkeep and housekeeping (conventional commit: chore)' + +- name: ci/cd + color: '00C853' + description: '๐Ÿš€ CI/CD changes - workflow automation and deployment (conventional commit: ci)' + +- name: build + color: '9E9E9E' + description: '๐Ÿ—๏ธ Build system changes - build tools, dependencies, config (conventional commit: build)' + +- name: revert + color: '78909C' + description: 'โช Revert changes - undoing previous commits (conventional commit: revert)' + +- name: breaking change + 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' + +- name: size/S + color: '76FF03' + description: '๐Ÿฟ๏ธ Small change - less than 50 lines modified' + +- name: size/M + color: 'FDD835' + description: '๐Ÿ• Medium change - less than 200 lines modified' + +- name: size/L + color: 'FF6D00' + description: '๐Ÿ˜ Large change - less than 500 lines modified' + +- name: size/XL + color: 'D50000' + description: '๐Ÿฆ– Extra large change - 500+ lines modified, consider splitting' + +# Additional useful labels +- name: dependencies + color: '2196F3' + description: '๐Ÿ“ฆ Dependency updates - library and package upgrades' + +- name: security + color: 'D50000' + description: '๐Ÿ”’ Security fixes - patches for vulnerabilities' + +- name: good first issue + color: '7C4DFF' + description: '๐Ÿ‘‹ Good for newcomers - great starting point for new contributors' + +- name: help wanted + color: '00BFA5' + description: '๐Ÿ™ Help wanted - seeking community input or assistance' + +- name: priority: high + color: 'FF1744' + description: '๐Ÿ”ด High priority - needs immediate attention' + +- name: priority: medium + color: 'FF9100' + description: '๐ŸŸก Medium priority - should be addressed soon' + +- name: priority: low + color: '69F0AE' + description: '๐ŸŸข Low priority - can be deferred' + +- name: question + color: 'E040FB' + description: 'โ“ Question - seeking clarification or discussion' + +- name: wontfix + color: 'CFD8DC' + description: 'โ›” Won\'t fix - this will not be worked on' + +- name: duplicate + color: 'B0BEC5' + description: '๐Ÿ”„ Duplicate - this issue or PR already exists elsewhere' + +- name: invalid + color: 'FDD835' + description: 'โŒ Invalid - not applicable or incorrect' + +- name: stale + color: 'EEEEEE' + description: 'โณ Stale - no recent activity, may be closed' + +- name: blocked + 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/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/pr-labeler.yml b/.github/workflows/pr-labeler.yml new file mode 100644 index 0000000..60c8160 --- /dev/null +++ b/.github/workflows/pr-labeler.yml @@ -0,0 +1,285 @@ +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': 'feature', + 'fix': 'bug', + 'docs': 'documentation', + 'style': 'style', + 'refactor': 'refactor', + 'perf': 'performance', + 'test': 'testing', + 'chore': 'chore', + '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) { + // 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, + }); + + 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') { + // 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, + }); + + 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)'); + } + } 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}`); + + // Get current labels + const { data: currentLabels } = await github.rest.issues.listLabelsOnIssue({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + }); + + 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 + 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 + 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`); + + // 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, + }); + + 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, + 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`); + } diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..6587df3 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,174 @@ +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: 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 + 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 README.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..fc0307c --- /dev/null +++ b/docs/VERSIONING.md @@ -0,0 +1,304 @@ +# 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 + - 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 + +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 + - Updates README version badge + - 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 + +### 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 + +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/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}" 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