diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a3ba19a..8a3f191 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -30,35 +30,21 @@ jobs: bundler-cache: true - name: Extract version from tag - id: get_version - run: | - VERSION=${GITHUB_REF#refs/tags/v} - echo "version=$VERSION" >> $GITHUB_OUTPUT - echo "Releasing version: $VERSION" + id: version + run: echo "value=${GITHUB_REF#refs/tags/v}" >> "$GITHUB_OUTPUT" - name: Verify version matches tag run: | GEM_VERSION=$(ruby -r./lib/git/markdown/version -e "puts GitMarkdown::VERSION") - TAG_VERSION="${{ steps.get_version.outputs.version }}" + TAG_VERSION="${{ steps.version.outputs.value }}" if [ "$GEM_VERSION" != "$TAG_VERSION" ]; then - echo "ERROR: Version mismatch!" - echo " Tag version: $TAG_VERSION" - echo " Gem version: $GEM_VERSION" - exit 1 - fi - echo "āœ“ Version $GEM_VERSION matches tag" - - - name: Verify changelog exists - run: | - if [ ! -f CHANGELOG.md ]; then - echo "ERROR: CHANGELOG.md is missing. Run 'rake release:bump' to generate it." + echo "Version mismatch: tag=$TAG_VERSION gem=$GEM_VERSION" exit 1 fi - name: Extract release notes from changelog - id: changelog run: | - VERSION="${{ steps.get_version.outputs.version }}" + VERSION="${{ steps.version.outputs.value }}" awk -v version="$VERSION" ' $0 ~ "^## \\[" version "\\]" { found=1 } @@ -67,20 +53,15 @@ jobs: ' CHANGELOG.md > release_notes.md if [ ! -s release_notes.md ]; then - echo "ERROR: No changelog entry found for v$VERSION" - echo "Run 'rake release:bump' to generate the changelog before creating the tag." + echo "No changelog entry found for v$VERSION" exit 1 fi - echo "āœ“ Found changelog entry for v$VERSION" - - - name: Create GitHub Release - uses: softprops/action-gh-release@v1 + - name: Create GitHub release + uses: softprops/action-gh-release@v2 with: body_path: release_notes.md generate_release_notes: false - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Publish to RubyGems uses: rubygems/release-gem@v1 diff --git a/.github/workflows/ruby.yml b/.github/workflows/ruby.yml index 07dbbbb..e24a02a 100644 --- a/.github/workflows/ruby.yml +++ b/.github/workflows/ruby.yml @@ -19,18 +19,34 @@ permissions: jobs: test: - runs-on: ubuntu-latest strategy: matrix: ruby-version: ["3", "4"] steps: - - uses: actions/checkout@v4 - - name: Set up Ruby - uses: ruby/setup-ruby@v1 - with: - ruby-version: ${{ matrix.ruby-version }} - bundler-cache: true # runs 'bundle install' and caches installed gems automatically - - name: Run tests - run: bundle exec rake + - uses: actions/checkout@v4 + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby-version }} + bundler-cache: true # runs 'bundle install' and caches installed gems automatically + - name: Run tests + run: bundle exec rake + + changelog: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Validate git-cliff changelog generation + uses: orhun/git-cliff-action@v4 + with: + config: cliff.toml + args: --unreleased --verbose + env: + OUTPUT: /tmp/changelog-preview.md + GITHUB_REPO: ${{ github.repository }} diff --git a/AGENT.md b/AGENT.md new file mode 100644 index 0000000..35073c2 --- /dev/null +++ b/AGENT.md @@ -0,0 +1,47 @@ +# Agent Guide + +Repository-specific rules for code-generation agents working on `git-markdown`. + +## Core workflow + +- Prefer surgical edits. Do not reformat unrelated code or reshuffle files. +- Read the actual call sites and CLI flows before changing behavior. +- Preserve the public CLI and generated Markdown shape unless the task explicitly requires a change. +- Fix root causes, not symptoms. +- Keep one canonical owner per behavior: + - remote parsing in `Git::Markdown::RemoteParser` + - auth/config in `Git::Markdown::Configuration` and `Git::Markdown::Credentials` + - provider fetching in `Git::Markdown::Providers::*` + - HTTP transport/response handling in `Git::Markdown::Api::*` + - output rendering in `Git::Markdown::Markdown::Generator` + +## Naming and code quality + +- Use one canonical term per concept across code, config, and docs. +- Do not add parallel wrappers or aliases for the same concept. +- Use clear names that describe the domain object or action; avoid vague names like `Manager`, `Helper`, or `Handler`. +- Keep classes small and intention-revealing. If logic starts spanning transport, parsing, and rendering concerns, split by responsibility rather than adding conditionals. +- Fail fast on unexpected states. Prefer explicit errors over silent fallbacks. +- When tests are hard to write, simplify the production design first instead of adding test-only indirection. + +## Testing and boundaries + +- Framework: Minitest. +- Keep WebMock enabled. Do not allow live HTTP in tests. +- Use VCR for outbound GitHub API interactions when fixtures are needed. +- Add or update minimal tests when behavior changes, especially for CLI output, parsing, and provider interactions. +- Use `standardrb` for Ruby style consistency. +- Run `bundle exec rake` before finishing. + +## Git commits and release + +- Conventional Commits are required and enforced by `commitlint`. +- Allowed commit types for normal work: `feat`, `fix`, `refactor`, `chore`, `test`, `docs`. +- Subject line: imperative, <= 80 chars, no trailing period. +- Add a body for non-trivial changes explaining why and how. +- Install hooks once per clone: `bundle exec lefthook install` +- Release flow: + - `bundle exec rake 'release:prepare[patch]'` + - changelog is generated by `git-cliff` + - publishing happens only in GitHub Actions + - this gem publishes only to RubyGems diff --git a/CHANGELOG.md b/CHANGELOG.md index daa9846..76c11bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,5 +9,3 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### šŸš€ Features - filter GitHub-native resolved comments via GraphQL - - diff --git a/Gemfile b/Gemfile index 38fdea3..fd5ddfa 100644 --- a/Gemfile +++ b/Gemfile @@ -5,8 +5,6 @@ source "https://rubygems.org" # Specify your gem's dependencies in git-markdown.gemspec gemspec -gem "gem-release", "~> 2.2" - group :development do gem "commitlint", require: false gem "lefthook", require: false diff --git a/README.md b/README.md index 33a90be..3a94f57 100644 --- a/README.md +++ b/README.md @@ -130,7 +130,7 @@ Note: `Gemfile.lock` is intentionally not tracked to avoid conflicts across Ruby ### Git hooks -We use [lefthook](https://lefthook.dev/) with the Ruby [commitlint](https://github.com/arandilopez/commitlint) gem to enforce Conventional Commits on every commit. CI also validates commit messages on pull requests and pushes to main/master. +We use [lefthook](https://lefthook.dev/) with the Ruby [commitlint](https://github.com/arandilopez/commitlint) gem to enforce Conventional Commits on every commit. We also use [Standard Ruby](https://standardrb.com/) to keep code style consistent. CI validates commit messages, Standard Ruby, tests, and git-cliff changelog generation on pull requests and pushes to main/master. Run the hook installer once per clone: @@ -146,27 +146,29 @@ rake install ## Release -Releases are triggered by pushed tags and use `CHANGELOG.md` for GitHub release notes. +Releases are tag-driven and published by GitHub Actions to RubyGems. Local release commands never publish directly. -If you want changelog automation, install `git-cliff` () locally and use it to update `CHANGELOG.md`. +Install [git-cliff](https://git-cliff.org/) locally before preparing a release. The release task regenerates `CHANGELOG.md` from Conventional Commits. -The release workflow expects a `## [X.Y.Z]` entry in `CHANGELOG.md` that matches the tag. Note: `release:prep` enforces a clean working tree and must run on the `main` or `master` branch; it will skip if the changelog has no changes. +Before preparing a release, make sure you are on `main` or `master` with a clean worktree. -Before bumping, install dependencies: +Then run one of: ```bash -bundle install +bundle exec rake 'release:prepare[patch]' +bundle exec rake 'release:prepare[minor]' +bundle exec rake 'release:prepare[major]' +bundle exec rake 'release:prepare[0.1.0]' ``` -Then run: +The task will: -```bash -# 1) Bump the version (commit created) -bundle exec gem bump -v X.Y.Z +1. Regenerate `CHANGELOG.md` with `git-cliff`. +1. Update `lib/git/markdown/version.rb`. +1. Commit the release changes. +1. Create and push the `vX.Y.Z` tag. -# 2) Prepare release (changelog + tag + push) -bundle exec rake release:prep -``` +The `Release` workflow then runs tests, publishes the gem to RubyGems, and creates the GitHub release from the changelog entry. ## Contributing diff --git a/Rakefile b/Rakefile index db8177e..f59cdcf 100644 --- a/Rakefile +++ b/Rakefile @@ -1,89 +1,99 @@ # frozen_string_literal: true -# Note: Releases are handled automatically by GitHub Actions when you push a tag (e.g., v0.2.0) -# See .github/workflows/release.yml for details require "bundler/gem_tasks" require "rake/testtask" require "standard/rake" require_relative "lib/git/markdown/version" +VERSION_PATH = File.expand_path("lib/git/markdown/version.rb", __dir__) +VALID_RELEASE_TARGETS = %w[major minor patch].freeze + Rake::TestTask.new(:test) do |t| t.libs << "test" t.libs << "lib" t.test_files = FileList["test/**/*_test.rb"] end -task default: %i[test standard] +def current_branch + `git branch --show-current`.strip +end -namespace :release do - desc "Full release: update changelog, bump version, commit, tag, and push" - task :bump, [:level] do |_t, args| - level = args[:level] || "patch" - valid_levels = %w[major minor patch pre] +def clean_worktree? + system("git diff --quiet") && system("git diff --cached --quiet") +end + +def release_version(target) + target = target.to_s.strip + raise ArgumentError, "Provide patch, minor, major, or an explicit X.Y.Z version." if target.empty? + + return target if target.match?(/\A\d+\.\d+\.\d+\z/) + + unless VALID_RELEASE_TARGETS.include?(target) + raise ArgumentError, "Invalid release target #{target.inspect}. Use #{VALID_RELEASE_TARGETS.join(", ")} or X.Y.Z." + end + + major, minor, patch = GitMarkdown::VERSION.split(".").map(&:to_i) + + case target + when "major" + "#{major + 1}.0.0" + when "minor" + "#{major}.#{minor + 1}.0" + when "patch" + "#{major}.#{minor}.#{patch + 1}" + end +end + +def update_version_file(version) + File.write( + VERSION_PATH, + <<~RUBY + # frozen_string_literal: true + + module GitMarkdown + VERSION = "#{version}" + end + RUBY + ) +end - unless valid_levels.include?(level) - abort "Invalid level: #{level}. Use: #{valid_levels.join(", ")}" - end +def update_changelog(version) + success = system("git-cliff", "-c", "cliff.toml", "--unreleased", "--tag", "v#{version}", "-o", "CHANGELOG.md") + raise "git-cliff failed. Install git-cliff and make sure cliff.toml is valid." unless success + raise "git-cliff did not update CHANGELOG.md. Ensure there are Conventional Commits since the last tag." if system("git", "diff", "--quiet", "--", "CHANGELOG.md") +end + +if Rake::Task.task_defined?("release") + Rake::Task["release"].clear +end - branch = `git rev-parse --abbrev-ref HEAD`.strip - unless ["main", "master"].include?(branch) - abort "Release must run on main or master. Current: #{branch}." - end +desc "Publishing is handled by GitHub Actions. Use release:prepare[...] instead." +task :release do + abort "Use `bundle exec rake 'release:prepare[patch]'` (or minor/major/X.Y.Z). Publishing runs in GitHub Actions after the tag is pushed." +end - unless system("git diff --quiet") && system("git diff --cached --quiet") - abort "Release requires a clean working tree." - end +namespace :release do + desc "Prepare a release: update CHANGELOG/version, commit, tag, and push. Accepts patch, minor, major, or X.Y.Z." + task :prepare, [:target] do |_task, args| + branch = current_branch + abort "Release must run on main or master. Current branch: #{branch.inspect}." unless %w[main master].include?(branch) + abort "Release requires a clean working tree." unless clean_worktree? + version = release_version(args[:target]) current = GitMarkdown::VERSION - parts = current.split(".").map(&:to_i) - - next_version = case level - when "major" - "#{parts[0] + 1}.0.0" - when "minor" - "#{parts[0]}.#{parts[1] + 1}.0" - when "patch" - "#{parts[0]}.#{parts[1]}.#{parts[2] + 1}" - when "pre" - "#{parts[0]}.#{parts[1]}.#{parts[2]}.pre.1" - end - - puts "=== Step 1: Updating CHANGELOG.md for v#{next_version} ===" - sh "git cliff -c cliff.toml --unreleased --tag v#{next_version} -o CHANGELOG.md" - - if system("git diff --quiet -- CHANGELOG.md") - puts "Warning: No changelog changes detected" - else - puts "āœ“ Changelog updated" - end - - puts "\n=== Step 2: Bumping version to #{next_version} ===" - - File.write( - "lib/git/markdown/version.rb", - <<~RUBY - # frozen_string_literal: true - - module GitMarkdown - VERSION = "#{next_version}" - end - RUBY - ) - - puts "āœ“ Version updated in lib/git/markdown/version.rb" - - puts "\n=== Step 3: Committing changes ===" - sh "git add CHANGELOG.md lib/git/markdown/version.rb" - sh "git commit -m \"chore(release): bump version to #{next_version}\"" - puts "āœ“ Changes committed" + abort "Release version #{version} is older than current version #{current}." if Gem::Version.new(version) < Gem::Version.new(current) - puts "\n=== Step 4: Creating and pushing tag ===" - sh "git tag -a v#{next_version} -m \"Release v#{next_version}\"" - sh "git push origin master" - sh "git push origin v#{next_version}" - puts "āœ“ Tag v#{next_version} created and pushed" + update_changelog(version) + update_version_file(version) - puts "\nāœ… Release v#{next_version} prepared successfully!" - puts "GitHub Actions will now build and publish the release." + sh "git add CHANGELOG.md lib/git/markdown/version.rb" + sh %(git commit -m "chore(release): prepare v#{version}") + sh %(git tag -a v#{version} -m "Release v#{version}") + sh "git push origin #{branch}" + sh "git push origin v#{version}" + rescue ArgumentError, RuntimeError => e + abort e.message end end + +task default: %i[test standard] diff --git a/git-markdown.gemspec b/git-markdown.gemspec index d768a76..eb93e8e 100644 --- a/git-markdown.gemspec +++ b/git-markdown.gemspec @@ -23,6 +23,8 @@ Gem::Specification.new do |spec| "changelog_uri" => "#{repo}/blob/#{branch}/CHANGELOG.md", "documentation_uri" => "#{repo}/blob/#{branch}/README.md", "funding_uri" => "https://www.reviato.com/", + "github_repo" => "ssh://github.com/ethos-link/git-markdown", + "allowed_push_host" => "https://rubygems.org", "rubygems_mfa_required" => "true" }