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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 8 additions & 27 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand All @@ -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
34 changes: 25 additions & 9 deletions .github/workflows/ruby.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
47 changes: 47 additions & 0 deletions AGENT.md
Original file line number Diff line number Diff line change
@@ -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
2 changes: 0 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,3 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### <!-- 0 -->🚀 Features
- filter GitHub-native resolved comments via GraphQL


2 changes: 0 additions & 2 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
28 changes: 15 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand All @@ -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` (<https://github.com/orhun/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

Expand Down
142 changes: 76 additions & 66 deletions Rakefile
Original file line number Diff line number Diff line change
@@ -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]
2 changes: 2 additions & 0 deletions git-markdown.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}

Expand Down
Loading