diff --git a/.claude/skills/release-docx/SKILL.md b/.claude/skills/release-docx/SKILL.md new file mode 100644 index 0000000..d94d7f6 --- /dev/null +++ b/.claude/skills/release-docx/SKILL.md @@ -0,0 +1,65 @@ +--- +name: release-docx +description: Cut a new release of the ruby-docx/docx gem. Use when the user wants to release, ship, publish, or tag a new version (e.g. "let's release", "リリースしちゃおう", "cut v0.13.0"). Bumps the version, pushes a tag, and lets GitHub Actions build the GitHub Release and publish to RubyGems. +--- + +# Release the docx gem + +Releasing ruby-docx/docx is **tag-driven**: pushing a `vX.Y.Z` tag triggers +`.github/workflows/release.yml`, which builds the gem (`rake build`), creates a +GitHub Release with auto-generated notes, and publishes to RubyGems via OIDC +trusted publishing (`rubygems/release-gem@v1`). No manual gem push or credentials. + +## Before you start + +- **Confirm the version number with the user.** Publishing to RubyGems is + effectively irreversible (you can only `gem yank`, not delete). Never guess. +- **Pick the version by semver.** A new public method/API (e.g. a new + `Paragraph#substitute`) is a **minor** bump, not a patch. Pure bug fixes are a + patch. Breaking changes are a major. State your recommendation when asking. +- Confirm what is unreleased: `git log --oneline vLAST..HEAD | grep 'Merge pull request'`. + +## Steps + +1. Make sure local `master` matches origin: + ```sh + git switch master && git fetch origin master && git merge --ff-only origin/master + ``` +2. Bump the version in `lib/docx/version.rb` (`VERSION = 'X.Y.Z'`). +3. Commit **directly to master** (no PR — this is the established pattern) with + the exact conventional message, then push: + ```sh + git add lib/docx/version.rb + git commit -m "Bump up the version to vX.Y.Z" + git push origin master + ``` +4. Tag and push the tag — **this is the trigger**: + ```sh + git tag vX.Y.Z + git push origin vX.Y.Z + ``` +5. Watch the release workflow to completion: + ```sh + run_id=$(gh run list --repo ruby-docx/docx --workflow release.yml --branch vX.Y.Z --limit 1 --json databaseId --jq '.[0].databaseId') + gh run watch "$run_id" --repo ruby-docx/docx --exit-status + ``` +6. Verify both outputs: + ```sh + gh release view vX.Y.Z --repo ruby-docx/docx --json tagName,isDraft,body + curl -s https://rubygems.org/api/v1/versions/docx/latest.json # expect {"version":"X.Y.Z"} + ``` + +## Notes + +- **Do NOT touch `CHANGELOG.md`** — it has been stale since v0.7.0; the + GitHub auto-generated release notes are the source of truth. +- Auto-generated notes credit each PR's title and its author. To get an original + contributor's handle into the notes, that handle must already be in the **PR + title** (the notes credit only the PR opener, who is the maintainer). So this + is decided at PR-creation time, not here. +- Two workflow runs fire on the bump (one for the `master` push, one for the + tag). The **tag** run is the one that does the actual release (its `release` + job is gated on `refs/tags/`). +- If the run fails after the tag is pushed, inspect with + `gh run view --repo ruby-docx/docx --log-failed`, fix forward, and re-tag + (e.g. a patch bump) — the bump commit and tag already exist. diff --git a/.claude/skills/tdd-development/SKILL.md b/.claude/skills/tdd-development/SKILL.md new file mode 100644 index 0000000..243d80e --- /dev/null +++ b/.claude/skills/tdd-development/SKILL.md @@ -0,0 +1,77 @@ +--- +name: tdd-development +description: The test-driven workflow for ALL code changes in ruby-docx/docx — features and bug fixes alike, not just bugs. Use whenever implementing, fixing, or changing behavior in this repo. Drives the full loop: sync master, write a failing test first (confirm red), implement to green, then branch → PR → CI → merge → cleanup. Invoke before starting any code change here. +--- + +# Test-driven development workflow (docx) + +Apply this to **every** behavior change in this repo — new features, bug fixes, +refactors that change behavior. Always write the test first and watch it fail +before writing the implementation. + +## Environment (read first) + +- Local Ruby is 4.0.x where **`bundle exec rake spec` is broken** (`ostruct` is + no longer a default gem). **Run tests with `bundle exec rspec spec`** directly, + or a single file/example with `-e`. +- Output is noisy with rubygems warnings. Filter them for readable results: + `bundle exec rspec spec 2>&1 | grep -vE 'warning:|previous definition'`. + +## The loop + +1. **Sync master and branch.** + ```sh + git switch master && git fetch origin master && git merge --ff-only origin/master + git switch -c + ``` +2. **Write a failing test first.** Add a spec (usually in + `spec/docx/document_spec.rb`) that reproduces the desired behavior or the bug. + Name a regression test after the issue, e.g. a comment `# Regression test for #NNN`. +3. **Confirm RED.** Run just that example and verify it fails for the expected + reason: + ```sh + bundle exec rspec spec/docx/document_spec.rb -e "" 2>&1 | grep -vE 'warning:|previous definition' + ``` +4. **Implement the minimal change** to make it pass. Keep it focused. +5. **Confirm GREEN** for the new test and **run the full suite** to catch + regressions: + ```sh + bundle exec rspec spec 2>&1 | grep -vE 'warning:|previous definition' | tail -4 + ``` +6. **Commit, push, open a PR.** End the commit body with the Co-Authored-By + trailer. Write a PR body explaining the bug/feature, the fix, and the tests; + add `Closes #NNN` when it resolves an issue. +7. **Check CI, then merge.** + ```sh + gh pr checks --repo ruby-docx/docx + gh pr merge --repo ruby-docx/docx --merge # merge commit, matching repo history + ``` +8. **Update master and clean up.** + ```sh + git switch master && git fetch origin master && git merge --ff-only origin/master + git branch -D && git push origin --delete + ``` + +## Conventions specific to this repo + +- **Test fixtures (.docx) can be crafted programmatically.** Read the entries of + an existing fixture with rubyzip, mutate `word/document.xml` / `word/styles.xml` + / `word/_rels/document.xml.rels` with Nokogiri, and rewrite the zip. Used to + build split-placeholder, malformed-rels, missing-styles, and bookmark fixtures. + Run such generators with `bundle exec ruby` and commit the resulting binary. +- **Keep new constructor arguments optional** (`def initialize(node, props = {}, + doc = nil)`) so existing direct instantiations and callers keep working + (backward compatibility — e.g. threading `document_properties` through Table → + Row → Cell → Paragraph). +- **Be honest about scope.** When a report (e.g. a fuzzer issue) spans several + causes, separate gem-side logic bugs (which you fix) from raw exceptions thrown + by underlying libraries (rubyzip/Nokogiri) on genuinely corrupt input (out of + scope). Say so in the PR rather than implying a total fix. +- **Don't state something is done before it is.** Order operations so any comment + or claim is true when posted (e.g. add a `Co-authored-by` trailer to the commit + *before* telling a contributor they are credited). + +## After a batch of merges + +A series of fixes/features usually warrants a release — see the `release-docx` +skill (confirm the version with the user; a new public method is a minor bump). diff --git a/.gitignore b/.gitignore index e39b2e6..1b2f352 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,7 @@ pkg/* doc/ vendor/ coverage/ + +# Claude Code: share skills (.claude/skills), keep local settings out of git +.claude/settings.json +.claude/settings.local.json diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..51ac6d1 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,58 @@ +# docx — project guide for Claude + +`docx` is a Ruby gem (ruby-docx/docx) for reading and editing `.docx` files: a +thin wrapper over rubyzip + Nokogiri. + +## Repo & access + +- `origin` points directly at the upstream repo `github.com/ruby-docx/docx` + (not a fork). The user is a **maintainer with push access**, so branches and + tags can be pushed to `origin` and PRs merged directly. + +## Environment gotchas + +- Local Ruby is 4.0.x where **`bundle exec rake spec` fails** (`ostruct` is no + longer a default gem). **Run tests with `bundle exec rspec spec`** instead + (or a single example with `-e`). +- Test output is noisy with rubygems warnings; filter for readability: + `... 2>&1 | grep -vE 'warning:|previous definition'`. + +## Conventions + +- **Always work test-first.** Every behavior change (feature or bug fix) follows + the `tdd-development` skill: write a failing test, confirm red, implement, + confirm green + full suite, then branch → PR → CI → merge → cleanup. +- **Keep new constructor arguments optional** (`def initialize(node, props = {}, + doc = nil)`) to preserve backward compatibility for existing callers. +- **Be honest about scope.** When a report spans multiple causes, separate + gem-side logic bugs (fix them) from raw exceptions thrown by rubyzip/Nokogiri + on genuinely corrupt input (out of scope) — and say which is which. +- **Don't claim something is done before it is.** Order operations so any comment + or status is true at the moment it's posted. + +## Releasing & contributor credit + +- Releases are tag-driven; use the `release-docx` skill. Confirm the version with + the user first (RubyGems publish is irreversible; a new public method is a + **minor** bump). Do **not** edit `CHANGELOG.md` (stale since v0.7.0; the GitHub + auto-generated notes are the source of truth). +- Auto-generated release notes credit each PR by its title and its opener (the + maintainer). To get an **original contributor's** handle into the notes, embed + the handle in the **PR title** (e.g. "... (original work by @user in #NN)") and + add a `Co-authored-by:` trailer — co-authors alone do not appear in the notes. + +## Current state & roadmap (as of v0.13.0, 2026-05-31) + +- Shipped: header/footer **reading** (`#headers`/`#footers`), **write-back** on + save, and **bookmark scanning in headers/footers**. All bug-labeled issues are + resolved; the only open issues are question/enhancement (#157, #91). +- Not yet done (good next work): + - Resolve the **main document part name via `[Content Types].xml`** instead of + globbing `word/document*.xml`; `#update` currently writes back to a + hardcoded `word/document.xml`, so Office 365 `document2.xml` is not updated. + - **Numbering** support (`word/numbering.xml`) — requested in #73. + +## Skills + +- `release-docx` — cut a new gem release (version bump → tag → CI publishes). +- `tdd-development` — the test-first workflow for any code change here.