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
183 changes: 183 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
name: Release

on:
release:
types: [published]

permissions:
contents: read

jobs:
test:
runs-on: ubuntu-latest
name: Test before release
strategy:
fail-fast: true
matrix:
ruby:
- "3.2"
- "3.3"
- "3.4"

steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: ruby/setup-ruby@v1
with:
ruby-version: ${{ matrix.ruby }}
bundler-cache: true
- run: bundle exec rake

build:
needs: test
runs-on: ubuntu-latest
outputs:
gem_name: ${{ steps.build.outputs.gem_name }}
version: ${{ steps.build.outputs.version }}

steps:
- uses: actions/checkout@v4

- uses: ruby/setup-ruby@v1
with:
ruby-version: "3.4"
bundler-cache: true

- name: Verify tag matches gem version
run: |
GEM_VERSION=$(ruby -r ./lib/docs_kit/version -e "puts DocsKit::VERSION")
TAG_VERSION="${{ github.event.release.tag_name }}"
TAG_VERSION="${TAG_VERSION#v}"
if [ "$GEM_VERSION" != "$TAG_VERSION" ]; then
echo "::error::Tag version ($TAG_VERSION) does not match gem version ($GEM_VERSION)"
exit 1
fi

- name: Build gem
id: build
run: |
gem build docs-kit.gemspec --strict
GEM_NAME=$(ls docs-kit-*.gem)
echo "gem_name=$GEM_NAME" >> "$GITHUB_OUTPUT"
TAG_VERSION="${{ github.event.release.tag_name }}"
echo "version=${TAG_VERSION#v}" >> "$GITHUB_OUTPUT"

- name: Verify gem contents
run: |
GEM_NAME=$(ls docs-kit-*.gem)
gem unpack "$GEM_NAME" --target /tmp/gem-verify

echo "=== File count ==="
find /tmp/gem-verify -type f | wc -l

echo "=== Checking for unwanted files ==="
UNWANTED=$(find /tmp/gem-verify \( -name ".git*" -o -name "*.gemspec" \) -print)
if [ -n "$UNWANTED" ]; then
echo "::error::Unwanted files found in gem:"
echo "$UNWANTED"
exit 1
fi

UNWANTED_DIRS=$(find /tmp/gem-verify -type d \( -name "spec" -o -name "test" \) -print)
if [ -n "$UNWANTED_DIRS" ]; then
echo "::error::Unwanted directories found in gem:"
echo "$UNWANTED_DIRS"
exit 1
fi

echo "No unwanted files found"

- name: Generate checksums
run: |
GEM_NAME=$(ls docs-kit-*.gem)
sha256sum "$GEM_NAME" > "$GEM_NAME.sha256"
sha512sum "$GEM_NAME" > "$GEM_NAME.sha512"
echo "=== SHA256 ===" && cat "$GEM_NAME.sha256"
echo "=== SHA512 ===" && cat "$GEM_NAME.sha512"

- name: Upload gem artifact
uses: actions/upload-artifact@v4
with:
name: gem
path: |
docs-kit-*.gem
docs-kit-*.gem.sha256
docs-kit-*.gem.sha512
if-no-files-found: error

publish-rubygems:
needs: build
runs-on: ubuntu-latest
environment: rubygems
permissions:
id-token: write
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true

steps:
- uses: actions/download-artifact@v4
with:
name: gem

- name: Verify checksums
run: |
sha256sum -c docs-kit-*.gem.sha256
sha512sum -c docs-kit-*.gem.sha512

- uses: ruby/setup-ruby@v1
with:
ruby-version: "3.4"

- name: Configure RubyGems trusted publishing credentials
uses: rubygems/configure-rubygems-credentials@bc6dd217f8a4f919d6835fcfefd470ef821f5c44 # v1.0.0

- name: Push to RubyGems with Sigstore attestation
run: |
GEM_NAME=$(ls docs-kit-*.gem)
VERSION="${GEM_NAME%.gem}"
VERSION="${VERSION#docs-kit-}"

if gem info docs-kit --exact --remote 2>/dev/null | grep -q "docs-kit ($VERSION)"; then
echo "::warning::Version $VERSION already published to RubyGems, skipping push"
else
gem exec sigstore-cli sign "$GEM_NAME" \
--bundle "$GEM_NAME.sigstore.json"

gem push "$GEM_NAME" --attestation "$GEM_NAME.sigstore.json"
fi

- name: Upload Sigstore bundle
uses: actions/upload-artifact@v4
with:
name: sigstore
path: docs-kit-*.gem.sigstore.json
if-no-files-found: error

upload-release-assets:
needs: [build, publish-rubygems]
runs-on: ubuntu-latest
permissions:
contents: write

steps:
- uses: actions/download-artifact@v4
with:
name: gem

- uses: actions/download-artifact@v4
with:
name: sigstore

- name: Upload assets to GitHub release
env:
GH_TOKEN: ${{ github.token }}
run: |
GEM_NAME="${{ needs.build.outputs.gem_name }}"
gh release upload "${{ github.event.release.tag_name }}" \
"$GEM_NAME" \
"$GEM_NAME.sha256" \
"$GEM_NAME.sha512" \
"$GEM_NAME.sigstore.json" \
--repo "${{ github.repository }}" \
--clobber
4 changes: 4 additions & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,10 @@ Metrics/BlockLength:
- "*.gemspec"
# Rails app template — one big after_bundle block by design.
- "lib/docs_kit/templates/**/*"
# The `rake release[X.Y.Z]` task is one long sequential release script
# (version bump → build → commit → push → gh release), matching the sibling
# gems (daisyui/phlex-reactive/pgbus). Its length is steps, not complexity.
- "Rakefile"

# Generators are procedural wiring — one method per install step — so they're
# naturally long and flat, not complex. Configuration is the per-site config
Expand Down
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@

### Added

- Release tooling matching the sibling gems (daisyui/phlex-reactive/pgbus): a
`rake release[X.Y.Z]` task (version bump → lockfile update → build-verify →
commit → push → GitHub Release; `pre`/`force` supported, `main`-only, clean-tree
guard) and `.github/workflows/release.yml`, which on `release: published` runs
the suite, content-checks the built gem, signs it with Sigstore, and publishes
to RubyGems over **OIDC trusted publishing** (no stored API token). See the
README "Releasing (maintainers)" section for the one-time trusted-publisher +
`rubygems` environment setup.
- Initial extraction of the shared docs-site chrome into `docs-kit`.
- `Docs::*` Phlex component kit: `Shell` (full-document drawer layout), `Sidebar`
(config-driven nav), `ThemeSwitcher`, `Icon`, `Code` (Rouge + inline theme),
Expand Down
36 changes: 36 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -802,6 +802,42 @@ import ReactiveController from "phlex/reactive/reactive_controller"
application.register("reactive", ReactiveController)
```

## Releasing (maintainers)

Cut a release with the version-bumping Rake task — never `gem push` by hand:

```bash
rake release[1.0.0] # bump → build-verify → commit → push → GitHub Release
rake release[1.1.0.rc1] # a pre-release (auto-flagged --prerelease)
rake release[1.0.0,force] # delete + re-create an existing tag/release
```

The task (on `main`, clean tree only) bumps `lib/docs_kit/version.rb`, updates the
lockfiles (incl. `docs/Gemfile.lock`), verifies `gem build --strict`, commits,
pushes, and creates the GitHub Release. Publishing the tag fires
`.github/workflows/release.yml`, which runs the suite, rebuilds + content-checks
the gem, signs it with Sigstore, and pushes to RubyGems over **OIDC trusted
publishing** (no API token stored anywhere).

### One-time setup (before the first release)

Trusted publishing needs two things wired once — the first release fails without
them:

1. **RubyGems pending trusted publisher.** On [rubygems.org](https://rubygems.org)
→ your profile → *Trusted Publishers* → *Create*, add a **pending** publisher
(works for a gem not yet pushed) with:
- Gem name: `docs-kit`
- Repository: `mhenrixon/docs-kit`
- Workflow filename: `release.yml`
- Environment: `rubygems`
2. **GitHub `rubygems` environment.** Repo *Settings → Environments → New
environment* named `rubygems` (the `publish-rubygems` job pins it and requests
`id-token: write`). Add reviewers there if you want a manual gate before a push.

After the first successful push the pending publisher converts to a normal one; no
further setup is needed for later releases.

## License

MIT.
Loading
Loading