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
38 changes: 38 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -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.
96 changes: 95 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,101 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed

- avoid rewriting existing IndexNow key files (#11)
- avoid rewriting existing IndexNow key files ([#11](https://github.com/ethos-link/indexmap/pull/11))



## [0.6.0] - 2026-05-01


### Added

- add the url count to google ping output ([#9](https://github.com/ethos-link/indexmap/pull/9))

- support named sitemap outputs ([#10](https://github.com/ethos-link/indexmap/pull/10))



## [0.5.0] - 2026-04-24


### Fixed

- namespace rake tasks and harden sitemap validation ([#8](https://github.com/ethos-link/indexmap/pull/8))



## [0.4.2] - 2026-04-23


### Fixed

- harden sitemap pinging and indexnow key handling ([#7](https://github.com/ethos-link/indexmap/pull/7))



## [0.4.1] - 2026-04-22


### Added

- add output to ping tasks ([#6](https://github.com/ethos-link/indexmap/pull/6))



## [0.4.0] - 2026-04-22


### Documentation

- improve the task messages ([#4](https://github.com/ethos-link/indexmap/pull/4))



## [0.3.1] - 2026-04-22


### Fixed

- fix changelog generation ([#2](https://github.com/ethos-link/indexmap/pull/2))

- harden indexmap runtime defaults and test coverage ([#3](https://github.com/ethos-link/indexmap/pull/3))



## [0.3.0] - 2026-04-22


### Added

- expand indexmap with sitemap parsing, validation, and search engine pinging



## [0.2.1] - 2026-04-21


### Fixed

- publish built gem in release workflow



## [0.2.0] - 2026-04-21


### Added

- add single-file sitemap mode



## [0.1.0] - 2026-04-21


### Added

- bootstrap indexmap gem



23 changes: 17 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -385,7 +385,12 @@ Note: `Gemfile.lock` is intentionally not tracked for this gem, following normal

### 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:

Expand All @@ -395,11 +400,16 @@ bundle exec lefthook 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:

Expand All @@ -412,12 +422,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/indexmap/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.

## License

Expand Down
108 changes: 96 additions & 12 deletions Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,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 = Indexmap::VERSION.split(".").map(&:to_i)
Expand All @@ -45,6 +54,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,
Expand All @@ -58,40 +111,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 = Indexmap::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/indexmap/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
Expand Down
3 changes: 2 additions & 1 deletion UPGRADE.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,8 @@ Custom storage backends must accept the documented `content_type:` keyword on
```ruby
class SitemapStorage
def write(filename, body, content_type:)
# Persist body under filename and store content_type when the backend uses it.
# Persist body under filename and store content_type when the backend
# uses it.
end

def read(filename)
Expand Down
4 changes: 4 additions & 0 deletions cliff.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ body = """
"""
trim = true

[[changelog.postprocessors]]
pattern = '\(#([0-9]+)\)'
replace = '([#${1}](https://github.com/ethos-link/indexmap/pull/${1}))'

[git]
conventional_commits = true
filter_unconventional = true
Expand Down
Loading
Loading