From 019739ad4d05d776666f04c114cc5046d8d99d59 Mon Sep 17 00:00:00 2001 From: Paulo Fidalgo Date: Tue, 12 May 2026 10:58:18 +0100 Subject: [PATCH] fix: preserve release changelog history Release preparation was overwriting CHANGELOG.md with only the next generated section. That hid older release notes and made future releases vulnerable to the same loss. Route changelog generation through git-cliff --prepend, restore the missing release entries from tag history, add tag validation, link future PR references, and document upgrade plus commit-message expectations. Verification: ruby -c Rakefile; ruby -c test/release_task_test.rb; git-cliff -c cliff.toml -o /tmp/crawlscope-changelog-smoke.md; git diff --check. Bundle-backed tests and Standard Ruby were blocked because required gems are not installed locally. --- AGENTS.md | 38 ++++++++++++++ CHANGELOG.md | 44 ++++++++++++++++ README.md | 23 +++++--- Rakefile | 108 +++++++++++++++++++++++++++++++++----- UPGRADE.md | 7 +++ cliff.toml | 4 ++ test/release_task_test.rb | 86 ++++++++++++++++++++++++++++++ 7 files changed, 292 insertions(+), 18 deletions(-) create mode 100644 AGENTS.md create mode 100644 UPGRADE.md create mode 100644 test/release_task_test.rb diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..237414e --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,38 @@ +# AI Agent Playbook + +Repository-specific rules for code-generation agents. Keep changes minimal, +validated, and aligned with this gem's public API. + +## Core Workflow + +- Prefer surgical edits. Do not reformat unrelated code or shuffle files. +- Read actual files, signatures, call sites, and tests before changing code. +- Preserve public APIs unless the requested change requires a contract update. +- Keep the gem reusable; do not add host-app-specific routes, models, or copy. +- Add or update focused tests when behavior changes. +- Do not commit secrets, credentials, tokens, or decrypted values. + +## Release And Upgrade + +- Release changes must preserve existing `CHANGELOG.md` history. +- Use the release task instead of hand-editing version files and tags. +- Changelog pull request references must link to PRs, not issues. +- When a change breaks or changes a public contract, update `UPGRADE.md` in + the same change with explicit host-app migration steps. +- Before finishing release-harness changes, run the focused release tests and + a `git-cliff` smoke check. + +## Commit Messages + +- Use Conventional Commits: `feat`, `fix`, `docs`, `test`, `refactor`, or + `chore`. +- Keep the subject imperative, specific, and under 72 characters. +- Leave a blank line between the subject and body. +- Write one coherent reason per commit; split unrelated work first. +- Use the body when the reasoning matters. Explain why the change exists, + what approach was taken, and what constraints or side effects matter. +- Wrap body lines at 72 characters so commit hooks and terminal tools stay + readable. +- Avoid vague subjects such as `misc fixes`, `updates`, or `cleanup` unless + the cleanup is the actual scoped purpose. +- Mention verification in the body when it materially helps future readers. diff --git a/CHANGELOG.md b/CHANGELOG.md index 051929b..2438a4d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,3 +28,47 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 +## [0.2.0] - 2026-04-24 + + +### Changed + +- simplify crawl and structured data boundaries + +- harden validation boundaries + + + + +### Fixed + +- handle child sitemaps + +- use URL for sitemap validation + + + +## [0.1.0] - 2026-04-23 + + +### Added + +- add crawlkit release-ready audit gem + +- add standalone validation commands + +- move default schema rules into crawlkit + + + + +### Changed + +- strengthen public API coverage + +- load shared test dependencies + +- rename crawlkit to crawlscope + + + diff --git a/README.md b/README.md index 41eb851..357d6fa 100644 --- a/README.md +++ b/README.md @@ -268,7 +268,12 @@ bundle exec rake ### 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. 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. +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: @@ -284,11 +289,16 @@ rake install ## Release -Releases are tag-driven and published by GitHub Actions to RubyGems. Local release commands never publish directly. +Releases are tag-driven and published by GitHub Actions to RubyGems. +Local release commands never publish directly. -Install [git-cliff](https://git-cliff.org/) locally before preparing a release. The release task regenerates `CHANGELOG.md` from Conventional Commits. +Install [git-cliff](https://git-cliff.org/) locally before preparing a +release. The release task prepends the next `CHANGELOG.md` section from +Conventional Commits. -Before preparing a release, make sure you are on `main` or `master` with a clean worktree. +Before preparing a release, make sure you are on `main` or `master` with a +clean worktree. If the release contains a breaking public-contract change, +update `UPGRADE.md` with the host-app migration steps first. Then run one of: @@ -301,12 +311,13 @@ bundle exec rake 'release:prepare[0.1.0]' The task will: -1. Regenerate `CHANGELOG.md` with `git-cliff`. +1. Prepend the next `CHANGELOG.md` section with `git-cliff`. 1. Update `lib/crawlscope/version.rb`. 1. Commit the release changes. 1. Create and push the `vX.Y.Z` tag. -The `Release` workflow then runs tests, publishes the gem to RubyGems, and creates the GitHub release from the changelog entry. +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 8c221c1..eac8d3d 100644 --- a/Rakefile +++ b/Rakefile @@ -24,14 +24,23 @@ def clean_worktree? system("git diff --quiet") && system("git diff --cached --quiet") end +def release_tag(version) + "v#{version}" +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? + if target.empty? + message = "Provide patch, minor, major, or an explicit X.Y.Z version." + raise ArgumentError, message + end 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." + message = "Invalid release target #{target.inspect}. Use " \ + "#{VALID_RELEASE_TARGETS.join(", ")} or X.Y.Z." + raise ArgumentError, message end major, minor, patch = Crawlscope::VERSION.split(".").map(&:to_i) @@ -46,6 +55,50 @@ def release_version(target) end end +def validate_release_version!(version, current) + if Gem::Version.new(version) <= Gem::Version.new(current) + message = "Release version #{version} must be newer than " \ + "current version #{current}." + raise ArgumentError, message + end + + tag = release_tag(version) + if local_release_tag_exists?(tag) + raise ArgumentError, "Release tag #{tag} already exists locally." + end + if remote_release_tag_exists?(tag) + raise ArgumentError, "Release tag #{tag} already exists on origin." + end +end + +def local_release_tag_exists?(tag) + system( + "git", + "rev-parse", + "--quiet", + "--verify", + "refs/tags/#{tag}", + out: File::NULL + ) +end + +def remote_release_tag_exists?(tag) + output = `#{remote_release_tag_command(tag)} 2>&1` + status = $? + + if status.success? + true + elsif status.exitstatus == 2 + false + else + raise "Could not check origin for #{tag}: #{output.strip}" + end +end + +def remote_release_tag_command(tag) + "git ls-remote --exit-code --tags origin refs/tags/#{tag}" +end + def update_version_file(version) File.write( VERSION_PATH, @@ -59,40 +112,71 @@ def update_version_file(version) ) end +def changelog_command(version) + [ + "git-cliff", + "-c", + "cliff.toml", + "--unreleased", + "--tag", + release_tag(version), + "--prepend", + "CHANGELOG.md" + ] +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") + success = system(*changelog_command(version)) + unless success + message = "git-cliff failed. Install git-cliff and make sure " \ + "cliff.toml is valid." + raise message + end + + if system("git", "diff", "--quiet", "--", "CHANGELOG.md") + message = "git-cliff did not update CHANGELOG.md. Ensure there are " \ + "Conventional Commits since the last tag." + raise message + end end if Rake::Task.task_defined?("release") Rake::Task["release"].clear end -desc "Publishing is handled by GitHub Actions. Use release:prepare[...] instead." +desc "Publishing is handled by CI. 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." + message = "Use `bundle exec rake 'release:prepare[patch]'` " \ + "(or minor/major/X.Y.Z). Publishing runs in GitHub Actions after " \ + "the tag is pushed." + abort message end namespace :release do - desc "Prepare a release: update CHANGELOG/version, commit, tag, and push. Accepts patch, minor, major, or X.Y.Z." + desc "Prepare a release: update changelog/version, commit, tag, and push." 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) + unless %w[main master].include?(branch) + message = "Release must run on main or master. Current branch: " \ + "#{branch.inspect}." + abort message + end abort "Release requires a clean working tree." unless clean_worktree? version = release_version(args[:target]) current = Crawlscope::VERSION - abort "Release version #{version} is older than current version #{current}." if Gem::Version.new(version) < Gem::Version.new(current) + validate_release_version!(version, current) update_changelog(version) update_version_file(version) + tag = release_tag(version) + sh "git add CHANGELOG.md lib/crawlscope/version.rb" sh %(git commit -m "chore(release): prepare v#{version}") - sh %(git tag -a v#{version} -m "Release v#{version}") + sh %(git tag -a #{tag} -m "Release #{tag}") sh "git push origin #{branch}" - sh "git push origin v#{version}" + sh "git push origin #{tag}" rescue ArgumentError, RuntimeError => e abort e.message end diff --git a/UPGRADE.md b/UPGRADE.md new file mode 100644 index 0000000..3a6cb49 --- /dev/null +++ b/UPGRADE.md @@ -0,0 +1,7 @@ +# Upgrade Guide + +Use this file for host-app migration notes when a release changes public +contracts, required setup, component locals, generated assets, or runtime +behavior. + +No special upgrade notes have been published yet. diff --git a/cliff.toml b/cliff.toml index 8a40f75..862b62e 100644 --- a/cliff.toml +++ b/cliff.toml @@ -14,6 +14,10 @@ body = """ """ trim = true +[[changelog.postprocessors]] +pattern = '\(#([0-9]+)\)' +replace = '([#${1}](https://github.com/ethos-link/crawlscope/pull/${1}))' + [git] conventional_commits = true filter_unconventional = true diff --git a/test/release_task_test.rb b/test/release_task_test.rb new file mode 100644 index 0000000..d66b4e6 --- /dev/null +++ b/test/release_task_test.rb @@ -0,0 +1,86 @@ +# frozen_string_literal: true + +require "test_helper" +require "rake" + +unless respond_to?(:release_version, true) + load File.expand_path("../Rakefile", __dir__) +end + +class ReleaseTaskTest < Minitest::Test + def test_release_version_increments_patch_from_current_version + major, minor, patch = Crawlscope::VERSION.split(".").map(&:to_i) + + assert_equal "#{major}.#{minor}.#{patch + 1}", release_version("patch") + end + + def test_release_version_accepts_explicit_semantic_version + assert_equal "0.3.0", release_version("0.3.0") + end + + def test_validate_release_version_rejects_current_version + error = assert_raises(ArgumentError) do + validate_release_version!("0.2.7", "0.2.7") + end + + assert_equal( + "Release version 0.2.7 must be newer than current version 0.2.7.", + error.message + ) + end + + def test_validate_release_version_rejects_existing_local_tag + @local_release_tag_exists = true + @remote_release_tag_exists = false + + error = assert_raises(ArgumentError) do + validate_release_version!("0.2.8", "0.2.7") + end + + assert_equal "Release tag v0.2.8 already exists locally.", error.message + end + + def test_validate_release_version_rejects_existing_remote_tag + @local_release_tag_exists = false + @remote_release_tag_exists = true + + error = assert_raises(ArgumentError) do + validate_release_version!("0.2.8", "0.2.7") + end + + assert_equal "Release tag v0.2.8 already exists on origin.", error.message + end + + def test_remote_release_tag_command_asks_git_to_fail_when_no_tag_matches + assert_equal( + "git ls-remote --exit-code --tags origin refs/tags/v0.2.8", + remote_release_tag_command("v0.2.8") + ) + end + + def test_changelog_command_prepends_the_next_release + assert_equal( + [ + "git-cliff", + "-c", + "cliff.toml", + "--unreleased", + "--tag", + "v0.2.8", + "--prepend", + "CHANGELOG.md" + ], + changelog_command("0.2.8") + ) + end + + private + + def local_release_tag_exists?(_tag) + @local_release_tag_exists || false + end + + def remote_release_tag_exists?(_tag) + @remote_release_tag_exists || false + end +end