docs-kit 1.1.1 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4fee711dcf42e9b92cf749684ea9b94c07ef9d614cf21746881f2c7402f744ea
4
- data.tar.gz: 76e246db5be838d3b31b8f08a98d033c57e3765d82900245723afc32fd4cad5f
3
+ metadata.gz: 04c64683910c10c37f660008f8891ca9f87296a0aea1ddc67f9d9a199975cccd
4
+ data.tar.gz: d357822c1df8899b50008f28366992bde0b9d6f622ec90975b5926f11d2de2d7
5
5
  SHA512:
6
- metadata.gz: 1a9cd702122f1e7af103b4279d7e3f4d45e8e662d5838931ac622383ce23609befd8557daddd2b3ac4afda772fc148cf0f5458546a8776b6bae3168408f59065
7
- data.tar.gz: 05ce8bb90585331cdaefa92973c36b67d55b2b6164aae03f55d4679a35d8ce7c4128295e4919a3775123cecaa923355baf1622481741ca3a4312394d5b0dd0a2
6
+ metadata.gz: a2b905a1b80c5e156ea7e652ba45cb4d2ec54c91ad040754112cdd481644b7698fd5356459faf06f46ac140c0c6562b8989039b69585b3f257a3b5629cfe0d49
7
+ data.tar.gz: 9837ae7885fb3c6ad358b585853aa7d0dee63b9807c574f15e8543f70ae5de6ecea4f5a2755391588a111fa5d05fcf2456a76b9040a648b86bcf3cf767700171
data/CHANGELOG.md CHANGED
@@ -2,6 +2,80 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ### Upgrading from 1.1
6
+
7
+ ```bash
8
+ bundle update docs-kit
9
+ bin/rails g docs_kit:install --sync # applies the 1.2 migration, prints what it can't
10
+ bun install && bun run build:css
11
+ bundle exec rspec
12
+ ```
13
+
14
+ - **Ruby 3.3 or newer is required.** docs-kit 1.2 won't install on 3.2. Move
15
+ `.ruby-version` and the Dockerfile's `ARG RUBY_VERSION` first (`--sync` warns
16
+ about either one if it's still below 3.3).
17
+ - **`--sync` raises the toolchain it scaffolded.** In a Dockerfile that still
18
+ carries docs-kit's old `ARG BUN_VERSION=1.3.2`, it bumps that to 1.4.2. In
19
+ `package.json`, it raises docs-kit's old `tailwindcss` / `@tailwindcss/cli
20
+ ^4.1.18` and `daisyui ^5.6.0` floors to `^4.3.3` / `^5.7.47`. Values you
21
+ picked yourself are never rewritten: an older one is printed as a warning,
22
+ and a newer one is left alone. Review the result with `git diff`.
23
+ - **Deploys pick up Node 24 actions and `ubuntu-26.04` automatically** when your
24
+ `deploy-docs.yml` calls `zoolutions/docs-kit/.github/workflows/deploy.yml@main`
25
+ (what `docs-kit new` scaffolds). `--sync` warns about a caller pinned to a tag
26
+ or SHA; move the pin forward.
27
+ - **Untrack `app/assets/stylesheets/tailwind.sources.css`** if it's committed:
28
+ `git rm --cached app/assets/stylesheets/tailwind.sources.css`. The generator
29
+ now gitignores it; `--sync` reminds you when it's still tracked.
30
+
31
+ ### Removed
32
+
33
+ - **Ruby 3.2 support.** `required_ruby_version` is now `>= 3.3` (3.2 reached
34
+ end of life in March 2026), and CI no longer tests it.
35
+
36
+ ### Changed
37
+
38
+ - **daisyui 2.x is allowed.** The dependency is now `>= 1.2, < 3`. 1.2.0 was
39
+ tagged but never reached RubyGems (its release run failed a stale spec), so
40
+ 1.2.1 is the first published 1.2 release.
41
+ - **The reusable deploy workflow runs on Node 24 actions.** `deploy.yml` now
42
+ uses `actions/checkout@v7`, `docker/setup-buildx-action@v4`,
43
+ `docker/login-action@v4`, `docker/build-push-action@v7` and
44
+ `webfactory/ssh-agent@v0.10.0`, clearing the Node 20 deprecation warning for
45
+ every site that calls it. Its jobs run on `ubuntu-26.04` (ahead of the
46
+ `ubuntu-latest` migration).
47
+ - **`docs_kit:install` scaffolds current toolchain floors.** The `package.json`
48
+ stub requires `tailwindcss`/`@tailwindcss/cli` `^4.3.3` and `daisyui`
49
+ `^5.7.47` (a spec keeps them in lockstep with the gem's own docs site), and
50
+ the generated `Dockerfile` installs Bun 1.4.2 (and falls back to Ruby 3.4.11
51
+ when the host Ruby version can't be read).
52
+ - **`docs_kit:install` gitignores the generated `tailwind.sources.css`.**
53
+ `bin/build-css` rewrites it on every build with machine-specific gem paths, so
54
+ a committed copy only churns. `--sync` adds the `.gitignore` entry and warns
55
+ when the file is still tracked. (#72)
56
+ - **`--sync` runs its first release migration** (1.1 → 1.2, above). The
57
+ migration registry no longer ships empty.
58
+
59
+ ### Fixed
60
+
61
+ - **`docs_kit:page` injected the registry line once per group.** In a registry
62
+ laid out as blank-line-separated groups (the layout `docs_kit:install`
63
+ produces), the "page line not followed by a page line" Regexp anchor matched
64
+ the end of *every* group and Thor's `inject_into_file` replaced every match.
65
+ The anchor is now the file's last `page` line as a String, so one run adds
66
+ exactly one line, at the end of the last group.
67
+
68
+ ### Added
69
+
70
+ - **`DocsUI::Code` infers the language from `filename:`.**
71
+ `DocsUI::Code(<<~YAML, filename: "config/deploy.yml")` now highlights as YAML
72
+ instead of Ruby: with no `lexer:`, the lexer is guessed from Rouge's filename
73
+ globs (`*.yml` → yaml, `Dockerfile` → docker, `*.sh` → shell, …). An explicit
74
+ `lexer:` still wins, and a block with neither (or an unguessable filename)
75
+ stays Ruby, so existing output is unchanged.
76
+
77
+ ## [1.1.1] and earlier
78
+
5
79
  ### Fixed
6
80
 
7
81
  - **SEO `og:image` 404.** The og:image tag pointed at the raw config path
data/README.md CHANGED
@@ -102,9 +102,17 @@ A site created before the stamp existed (no comment) is treated as the earliest
102
102
  version, so it gets every migration. Migrations are **warn-only-safe** exactly
103
103
  like the drift report: what a step can't safely automate it prints as a manual
104
104
  checklist (`migration steps to apply by hand:`), never a destructive rewrite of
105
- a line you've edited. There are no migrations to apply yet — the mechanism ships
106
- ahead of the first release that needs one, so the upgrade path is already in
107
- place.
105
+ a line you've edited. A value is only rewritten when it is exactly what an older
106
+ docs-kit generated; anything you picked yourself is reported, never changed.
107
+
108
+ Every release that needs site changes lists them under **"Upgrading from X.Y"**
109
+ at the top of its [CHANGELOG](CHANGELOG.md) entry. That's what `--sync` automates
110
+ and what it leaves to you. Read it before a `bundle update docs-kit` that crosses
111
+ a minor version.
112
+
113
+ | Release | `--sync` does | You do |
114
+ |---------|---------------|--------|
115
+ | 1.2 | Raises the Bun and Tailwind/daisyUI floors that docs-kit generated. Warns about Ruby below 3.3, hand-picked older floors, and a deploy workflow pinned off `@main` | Move to Ruby >= 3.3 before updating. `git rm --cached` a tracked `tailwind.sources.css` |
108
116
 
109
117
  ### One-time cleanup for sites created before these landed
110
118
 
@@ -118,6 +126,7 @@ generators:
118
126
  | `app/helpers/icon_helper.rb` | docs-kit renders icons via rails_icons (`DocsUI::Icon`) | Delete the file. |
119
127
  | Hand-pinned docs-kit lines in `config/importmap.rb` | the engine auto-pins the `docs-nav` controller and its assets | Delete the manual `pin`/`pin_all_from` lines for docs-kit. |
120
128
  | `Dockerfile` stamped by an older docs-kit (`# docs-kit Dockerfile vX.Y.Z`) | docs-kit ships an optimized, multi-stage Dockerfile; a stale copy misses image-size wins | Diff yours against the current template (`lib/generators/docs_kit/install/templates/Dockerfile.tt` in the gem), adopt the changes or replace it. See [Upgrading your Dockerfile](#upgrading-your-dockerfile). |
129
+ | `app/assets/stylesheets/tailwind.sources.css` committed to git | `bin/build-css` regenerates it on every build with machine-specific absolute gem paths — the committed copy churns per machine/Ruby and no build consumes it. The generator adds the `.gitignore` entry, but gitignoring doesn't untrack an already-committed copy. | `git rm --cached app/assets/stylesheets/tailwind.sources.css` and commit. |
121
130
 
122
131
  ### Upgrading your Dockerfile
123
132
 
@@ -433,6 +442,10 @@ DocsUI::Section("Add the gem", id: "add", description: …) # title positional
433
442
  DocsUI::Code(source, lexer: :ruby, filename: "Gemfile") # source positional
434
443
  ```
435
444
 
445
+ `DocsUI::Code` infers the language from `filename:` (`config/deploy.yml` →
446
+ YAML, `Dockerfile` → docker, `bin/setup.sh` → shell); `lexer:` overrides the
447
+ guess, and with neither the block is highlighted as Ruby.
448
+
436
449
  For the two wrappers that take **no** positional argument — prose and a
437
450
  multi-language example — `DocsUI::Page` gives you lowercase helpers so a block
438
451
  needs no parens:
@@ -930,7 +943,7 @@ jobs:
930
943
  ```yaml
931
944
  service: <repo>
932
945
  image: zoolutions/<repo>
933
- minimum_version: 4.0.0 # dash 4: dash-proxy identity + in-place host migration
946
+ minimum_version: 4.0.7 # dash 4.0.7: doctor stale-proxy recovery + registry credential fail-fast
934
947
  retain_containers: 2
935
948
  error_pages_path: public # 502/503/504.html served during a deploy gap
936
949
  registry: { server: ghcr.io, username: mhenrixon, password: [DASH_REGISTRY_PASSWORD] }
@@ -1082,17 +1095,21 @@ application.register("reactive", ReactiveController)
1082
1095
 
1083
1096
  ## Releasing (maintainers)
1084
1097
 
1085
- Cut a release with the version-bumping Rake task — never `gem push` by hand:
1098
+ Cut a release with `bin/release`. Never `gem push` by hand:
1086
1099
 
1087
1100
  ```bash
1088
- rake release[1.0.0] # bump → build-verify → commit → push → GitHub Release
1089
- rake release[1.1.0.rc1] # a pre-release (auto-flagged --prerelease)
1090
- rake release[1.0.0,force] # delete + re-create an existing tag/release
1101
+ bin/release list # last releases + what each bump would give
1102
+ bin/release --dry-run # the version it would cut + the changes since the last tag
1103
+ bin/release # patch bump (minor / major / 1.3.0.rc1 for others)
1104
+ bin/release 1.2.0 --force # delete + re-create an existing tag/release
1091
1105
  ```
1092
1106
 
1093
- The task (on `main`, clean tree only) bumps `lib/docs_kit/version.rb`, updates the
1094
- lockfiles (incl. `docs/Gemfile.lock`), verifies `gem build --strict`, commits,
1095
- pushes, and creates the GitHub Release. Publishing the tag fires
1107
+ `bin/release` checks you're on a clean, up-to-date `main`, confirms, and hands off
1108
+ to `rake release[X.Y.Z]` (`rakelib/release.rake`). That task bumps
1109
+ `lib/docs_kit/version.rb` and the `docs-kit` pin in `docs/Gemfile.lock` (in place,
1110
+ no re-resolve), verifies `gem build --strict`, commits, pushes, and creates the
1111
+ GitHub Release. Both files belong to the zoolutions release kit that every gem
1112
+ shares; see [RELEASE_KIT.md](RELEASE_KIT.md) to adopt or upgrade it elsewhere. Publishing the tag fires
1096
1113
  `.github/workflows/release.yml`, which runs the suite, rebuilds + content-checks
1097
1114
  the gem, signs it with Sigstore, and pushes to RubyGems over **OIDC trusted
1098
1115
  publishing** (no API token stored anywhere).
@@ -9,12 +9,16 @@ module DocsUI
9
9
  # stylesheet asset is required.
10
10
  #
11
11
  # render DocsUI::Code.new(ruby_source) # ruby, no title
12
- # render DocsUI::Code.new(py, lexer: :python, filename: "a.py") # any language
12
+ # render DocsUI::Code.new(yaml, filename: "config/deploy.yml") # yaml, inferred
13
+ # render DocsUI::Code.new(py, lexer: :python, filename: "a.py") # explicit wins
13
14
  #
14
- # Any language Rouge knows (~200 lexers) works by its name or alias — python,
15
- # go, rust, elixir, kotlin, swift, json, dockerfile, ... — no allowlist. Add
16
- # friendly lexer aliases via DocsKit.configure (code_lexer_aliases). An unknown
17
- # language falls back to plaintext (never raises). (Tab labels are a
15
+ # The language resolves in order: an explicit `lexer:`; a guess from the
16
+ # `filename:` (Rouge's own filename globs — *.yml → yaml, Dockerfile → docker,
17
+ # *.sh → shell, …); else ruby. Any language Rouge knows (~200 lexers) works by
18
+ # its name or alias — python, go, rust, elixir, kotlin, swift, json,
19
+ # dockerfile, ... — no allowlist. Add friendly lexer aliases via
20
+ # DocsKit.configure (code_lexer_aliases). An unknown language falls back to
21
+ # plaintext (never raises). (Tab labels are a
18
22
  # DocsUI::Example concern — set via code_language_labels, not here; Code has no
19
23
  # label, only a filename.)
20
24
  class Code < Phlex::HTML
@@ -22,7 +26,7 @@ module DocsUI
22
26
 
23
27
  FORMATTER = Rouge::Formatters::HTML.new
24
28
 
25
- def initialize(source, lexer: :ruby, filename: nil)
29
+ def initialize(source, lexer: nil, filename: nil)
26
30
  @source = source.to_s.strip
27
31
  @lexer = lexer
28
32
  @filename = filename
@@ -66,11 +70,26 @@ module DocsUI
66
70
  end
67
71
  end
68
72
 
69
- # Resolve @lexer to a Rouge lexer instance. Order: an explicit Rouge::Lexer
70
- # class/instance passed through; a configured friendly alias; Rouge's own
71
- # registry (name/alias); then the configured fallback (plaintext).
73
+ # Resolve the lexer to a Rouge lexer instance. Order: an explicit Rouge::Lexer
74
+ # class/instance passed through; an explicit name via a configured friendly
75
+ # alias → Rouge's own registry → the configured fallback (plaintext); with no
76
+ # `lexer:` given, a guess from the filename; else the ruby default.
72
77
  def lexer
73
- explicit_lexer || (find_lexer(@lexer.to_s) || Rouge::Lexers::PlainText).new
78
+ explicit_lexer ||
79
+ (@lexer && (find_lexer(@lexer.to_s) || Rouge::Lexers::PlainText).new) ||
80
+ guessed_lexer ||
81
+ Rouge::Lexers::Ruby.new
82
+ end
83
+
84
+ # A lexer inferred from @filename via Rouge's declared filename globs, or nil
85
+ # when there is no filename, nothing matches, or the match is ambiguous.
86
+ # (Lexer.guesses, not Lexer.guess: the latter answers PlainText for a
87
+ # no-match, which is indistinguishable from a real guess.)
88
+ def guessed_lexer
89
+ return nil if @filename.nil?
90
+
91
+ guesses = Rouge::Lexer.guesses(filename: @filename.to_s)
92
+ guesses.size == 1 ? guesses.first.new : nil
74
93
  end
75
94
 
76
95
  # A Rouge::Lexer instance passed directly (class or instance), else nil.
@@ -64,7 +64,7 @@ after_bundle do
64
64
 
65
65
  # dash 4 renamed the on-host proxy (kamal-proxy → dash-proxy) and migrates a
66
66
  # host in place; an older CLI must not deploy this config.
67
- minimum_version: 4.0.0
67
+ minimum_version: 4.0.7
68
68
 
69
69
  # A stateless docs site never rolls back far — keep the host tidy.
70
70
  retain_containers: 2
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DocsKit
4
- VERSION = "1.1.1"
4
+ VERSION = "1.2.1"
5
5
  end
@@ -57,6 +57,12 @@ module DocsKit
57
57
 
58
58
  def self.synced_stamp(version = DocsKit::VERSION) = "# docs-kit synced: v#{version}"
59
59
 
60
+ # The generated Tailwind @source globs file bin/build-css rewrites on
61
+ # every build — gitignored fleet-wide (see ignore_generated_css_sources).
62
+ # The covering/negation line regexes live beside the path in SyncReport,
63
+ # which shares them for the tracked-file drift check.
64
+ TAILWIND_SOURCES = SyncReport::TAILWIND_SOURCES
65
+
60
66
  # The RuboCop wiring docs-kit injects. REQUIRE loads the cops;
61
67
  # INHERIT_GEM/INHERIT_PATH enable + scope them (see config/rubocop/docs_kit.yml).
62
68
  RUBOCOP_REQUIRE = "docs_kit/rubocop"
@@ -192,6 +198,35 @@ module DocsKit
192
198
  create_file "app/assets/builds/.keep", ""
193
199
  end
194
200
 
201
+ # Gitignore the bin/build-css-generated @source globs file (#71). It
202
+ # carries machine-specific absolute gem paths, so a committed copy churns
203
+ # on every rebuild by a different machine/Ruby — and no build consumes it
204
+ # (.dockerignore excludes it; every build path runs bin/build-css first).
205
+ # Additive + idempotent (tolerant of a hand-added entry with or without
206
+ # the leading slash), and NOT --sync-guarded: the ignore is the fleet-wide
207
+ # upgrade this step exists to ship. Untracking an already-committed copy
208
+ # is the site's call — SyncReport warns with the exact command instead
209
+ # (the generator never mutates git state).
210
+ def ignore_generated_css_sources
211
+ entry = "# Generated by bin/build-css (resolved gem @source globs).\n/#{TAILWIND_SOURCES}\n"
212
+ path = File.join(destination_root, ".gitignore")
213
+ return create_file(".gitignore", entry) unless File.exist?(path)
214
+
215
+ # Last-match-wins, like git reads the file: an EFFECTIVE `!` unignore is
216
+ # the site's deliberate opt-out — appending our entry after it would
217
+ # become the last matching rule and silently defeat the hand-edit, so
218
+ # back off (SyncReport skips its nag too). An effective ignore is done.
219
+ case SyncReport.tailwind_sources_rule(File.read(path))
220
+ when :negate
221
+ say_status(:skip, ".gitignore negates tailwind.sources.css (!) — respecting the site's opt-out",
222
+ :yellow)
223
+ when :ignore
224
+ say_status(:identical, ".gitignore (tailwind.sources.css)", :blue)
225
+ else
226
+ append_to_file ".gitignore", "\n#{entry}"
227
+ end
228
+ end
229
+
195
230
  # Install the `docs_kit:og` rake task — gem-owned wiring, refreshed on every
196
231
  # run so a site picks up task fixes. It does NOT ship an OG image: the
197
232
  # social-share image is SITE content, generated into the site's OWN
@@ -539,9 +574,9 @@ module DocsKit
539
574
  "watch:css": "bin/build-css --watch"
540
575
  },
541
576
  "devDependencies": {
542
- "@tailwindcss/cli": "^4.1.18",
543
- "daisyui": "^5.6.0",
544
- "tailwindcss": "^4.1.18"
577
+ "@tailwindcss/cli": "^4.3.3",
578
+ "daisyui": "^5.7.47",
579
+ "tailwindcss": "^4.3.3"
545
580
  }
546
581
  }
547
582
  JSON
@@ -578,7 +613,7 @@ module DocsKit
578
613
  # The RUBY_VERSION build ARG default: the host's running Ruby (X.Y.Z), so a
579
614
  # site's image matches its dev Ruby. Used in Dockerfile.tt.
580
615
  def ruby_version_arg
581
- RUBY_VERSION[/\d+\.\d+\.\d+/] || "3.4.2"
616
+ RUBY_VERSION[/\d+\.\d+\.\d+/] || "3.4.11"
582
617
  end
583
618
 
584
619
  # True when the site bundles Thruster (HTTP caching + compression +
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "migration"
4
+ require_relative "migrations/v1_2_0"
4
5
  require_relative "../../../docs_kit/version"
5
6
 
6
7
  module DocsKit
@@ -18,24 +19,26 @@ module DocsKit
18
19
  # re-run on EVERY sync forever. It can't legitimately exist anyway (a site
19
20
  # can't have "arrived" at a release it doesn't have), so it's filtered out.
20
21
  #
21
- # `.default` is the registry the generator uses. It SHIPS EMPTY at 1.0.x —
22
- # the mechanism (stamp the synced version, detect the gap, run ordered
23
- # transforms) is the deliverable; the first concrete `1.x → 1.y` transform is
24
- # a one-line `Migration.new(...)` addition here once a release needs one.
22
+ # `.default` is the registry the generator uses. Each release that changes
23
+ # what a site must carry adds one `Migration.new(...)` entry to MIGRATIONS,
24
+ # its transform in migrations/vX_Y_Z.rb.
25
25
  class MigrationRegistry
26
26
  def initialize(migrations = [])
27
27
  @migrations = migrations.sort_by(&:to)
28
28
  end
29
29
 
30
- # The registry the install generator runs during `--sync`. Empty today.
30
+ # The registry the install generator runs during `--sync`.
31
31
  def self.default
32
32
  @default ||= new(MIGRATIONS)
33
33
  end
34
34
 
35
- # No migrations to register yet — the mechanism is the 1.0 deliverable.
36
- # Add ordered `Migration.new(to: "1.x.0", description: "...") { ... }`
37
- # entries here as future releases change config knobs, routes, or templates.
38
- MIGRATIONS = [].freeze
35
+ # Ordered `Migration.new(to: "1.x.0", description: "...") { ... }` entries, one
36
+ # per release that changes config knobs, routes, templates, or toolchain.
37
+ MIGRATIONS = [
38
+ Migration.new(to: "1.2.0", description: Migrations::V1_2_0::DESCRIPTION) do |root, _generator|
39
+ Migrations::V1_2_0.call(root)
40
+ end
41
+ ].freeze
39
42
 
40
43
  # The migrations a site last synced at `from_version` still needs, ascending
41
44
  # by version — those in `(from_version, upto]`. `upto` defaults to the
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DocsKit
4
+ module Generators
5
+ module Migrations
6
+ # The 1.1 → 1.2 upgrade. docs-kit 1.2 needs Ruby >= 3.3, scaffolds Bun 1.4.2
7
+ # and newer Tailwind/daisyUI floors, and its reusable deploy workflow moved to
8
+ # Node 24 actions (only callers that track `@main` get that).
9
+ #
10
+ # Warn-only-safe: a value is rewritten only when it is EXACTLY what an older
11
+ # docs-kit generated (so it can't be a hand edit); anything else older is
12
+ # handed back as a warning, and a newer value is left alone.
13
+ module V1_2_0 # rubocop:disable Naming/ClassAndModuleCamelCase -- named after the release
14
+ DESCRIPTION = "docs-kit 1.2: Ruby >= 3.3, Bun 1.4.2, Tailwind 4.3 / daisyUI 5.7 floors, Node 24 deploy"
15
+ MIN_RUBY = Gem::Version.new("3.3")
16
+ BUN = { from: "1.3.2", to: "1.4.2" }.freeze
17
+ PACKAGE_FLOORS = {
18
+ "tailwindcss" => { from: "^4.1.18", to: "^4.3.3" },
19
+ "@tailwindcss/cli" => { from: "^4.1.18", to: "^4.3.3" },
20
+ "daisyui" => { from: "^5.6.0", to: "^5.7.47" }
21
+ }.freeze
22
+ DEPLOY_CALL = %r{zoolutions/docs-kit/\.github/workflows/deploy\.yml@(\S+)}
23
+
24
+ module_function
25
+
26
+ # Returns the warnings it could not safely automate.
27
+ def call(root)
28
+ bun(root) + ruby(root) + package_floors(root) + deploy_pins(root)
29
+ end
30
+
31
+ def bun(root)
32
+ edit(root, "Dockerfile") do |source, warnings|
33
+ current = source[/^ARG BUN_VERSION=(\S+)$/, 1]
34
+ next source unless current && older?(current, BUN[:to])
35
+ next source.sub("ARG BUN_VERSION=#{current}", "ARG BUN_VERSION=#{BUN[:to]}") if current == BUN[:from]
36
+
37
+ warnings << "Dockerfile: ARG BUN_VERSION=#{current} is older than the #{BUN[:to]} docs-kit now " \
38
+ "scaffolds; raise it (a bun.lock written by a newer Bun can fail " \
39
+ "`bun install --frozen-lockfile`)"
40
+ source
41
+ end
42
+ end
43
+
44
+ def ruby(root)
45
+ {
46
+ "Dockerfile" => read(root, "Dockerfile")&.[](/^ARG RUBY_VERSION=\S+$/),
47
+ ".ruby-version" => read(root, ".ruby-version")&.strip
48
+ }.filter_map do |file, value|
49
+ next unless value && below_min_ruby?(value.delete_prefix("ARG RUBY_VERSION="))
50
+
51
+ "#{file}: #{value} — docs-kit 1.2 requires Ruby >= #{MIN_RUBY}; move the site to 3.3 or newer"
52
+ end
53
+ end
54
+
55
+ def package_floors(root)
56
+ edit(root, "package.json") do |source, warnings|
57
+ PACKAGE_FLOORS.reduce(source) do |json, (name, floor)|
58
+ raise_floor(json, name, floor, warnings)
59
+ end
60
+ end
61
+ end
62
+
63
+ def raise_floor(json, name, floor, warnings)
64
+ pair = /("#{Regexp.escape(name)}"\s*:\s*")([^"]+)(")/
65
+ current = json[pair, 2]
66
+ return json unless current && older?(current, floor[:to])
67
+ if current == floor[:from]
68
+ return json.sub(pair) { "#{Regexp.last_match(1)}#{floor[:to]}#{Regexp.last_match(3)}" }
69
+ end
70
+
71
+ warnings << "package.json: #{name} #{current} is below the #{floor[:to]} docs-kit now builds against"
72
+ json
73
+ end
74
+
75
+ def deploy_pins(root)
76
+ Dir[File.join(root, ".github/workflows/*.{yml,yaml}")].filter_map do |path|
77
+ ref = File.read(path)[DEPLOY_CALL, 1]
78
+ next if ref.nil? || ref == "main"
79
+
80
+ "#{File.basename(path)}: calls docs-kit's deploy.yml@#{ref}; the Node 24 actions and " \
81
+ "ubuntu-26.04 runners are on @main — move the pin forward (or track @main)"
82
+ end
83
+ end
84
+
85
+ # Yields [source, warnings] for an existing file; writes back what the block returns.
86
+ def edit(root, file)
87
+ source = read(root, file)
88
+ return [] unless source
89
+
90
+ warnings = []
91
+ updated = yield(source, warnings)
92
+ File.write(File.join(root, file), updated) unless updated == source
93
+ warnings
94
+ end
95
+
96
+ def read(root, file)
97
+ path = File.join(root, file)
98
+ File.exist?(path) ? File.read(path) : nil
99
+ end
100
+
101
+ # "^5.6.0" / "~> 4.1" / "3.2.4" / "ruby-3.2.4" → its version, or nil when it
102
+ # isn't a plain version (a tag like "latest", a git URL, a range).
103
+ def version_of(value)
104
+ number = value.to_s[/\A(?:ruby-)?[\^~]?>?\s*(\d+(?:\.\d+)*)\z/, 1]
105
+ number && Gem::Version.new(number)
106
+ end
107
+
108
+ def older?(current, target)
109
+ current_version = version_of(current)
110
+ current_version ? current_version < version_of(target) : false
111
+ end
112
+
113
+ def below_min_ruby?(value)
114
+ version = version_of(value)
115
+ version ? version < MIN_RUBY : false
116
+ end
117
+ end
118
+ end
119
+ end
120
+ end
@@ -17,10 +17,54 @@ module DocsKit
17
17
  # - a dead IconHelper copy — the gem renders icons via rails_icons.
18
18
  # - a Dockerfile stamped by an OLDER docs-kit than the gem now ships — the
19
19
  # site should diff against the current template and adopt the improvements.
20
+ # - a git-tracked tailwind.sources.css — generated per-build with machine
21
+ # absolute gem paths (#71); gitignored now, but untracking a committed
22
+ # copy stages a deletion, so the site runs `git rm --cached` itself.
20
23
  class SyncReport
21
24
  APPLICATION_CONTROLLER = "app/controllers/application_controller.rb"
22
25
  ICON_HELPER = "app/helpers/icon_helper.rb"
23
26
  DOCKERFILE = "Dockerfile"
27
+ TAILWIND_SOURCES = "app/assets/stylesheets/tailwind.sources.css"
28
+
29
+ # A .gitignore line covering the generated file per gitignore semantics:
30
+ # the anchored path (leading slash optional — a slash-containing pattern
31
+ # is root-anchored either way) or the bare filename (no slash → matches
32
+ # at any depth), each optionally **/-prefixed. Deliberately NOT a full
33
+ # gitignore matcher: an exotic broader glob (`app/assets/stylesheets/*`)
34
+ # is missed at the cost of one redundant, harmless line. This regex
35
+ # drives the generator's APPEND decision only; the drift warning asks
36
+ # git itself (see #ignored_by_git?, scoped to the committed .gitignore
37
+ # files so a user's personal excludes never decide repo content).
38
+ # `[ \t\r]*` (not `[ \t]*`): `$` matches before `\n` but never past a
39
+ # `\r`, so the class must consume the CR of a CRLF-checked-out file.
40
+ TAILWIND_SOURCES_COVER =
41
+ "(?:(?:/|\\*\\*/)?#{Regexp.escape(File.dirname(TAILWIND_SOURCES))}/|(?:\\*\\*/)?)" \
42
+ "#{Regexp.escape(File.basename(TAILWIND_SOURCES))}[ \t\r]*".freeze
43
+ TAILWIND_SOURCES_IGNORED_RE = /^#{TAILWIND_SOURCES_COVER}$/
44
+ # An explicit `!` unignore of the file — the site's deliberate opt-out
45
+ # from the fleet convention (it wants the file committed). The
46
+ # generator's append respects it when it's the file's effective rule
47
+ # (see .tailwind_sources_rule); the drift warning asks git instead
48
+ # (#ignored_by_git?), which resolves full pattern semantics.
49
+ TAILWIND_SOURCES_NEGATED_RE = /^!#{TAILWIND_SOURCES_COVER}$/
50
+
51
+ # The file's effective disposition among the recognized .gitignore lines,
52
+ # honoring git's last-match-wins: `:negate` (the site's genuine opt-out),
53
+ # `:ignore` (already covered), or nil (no recognized line). A `!` line
54
+ # overridden by a LATER ignore line is dead — git ignores the file, so
55
+ # treating it as an opt-out would misreport the site's intent. Drives the
56
+ # generator's append decision only (the drift check asks git itself).
57
+ def self.tailwind_sources_rule(gitignore_content)
58
+ rule = nil
59
+ gitignore_content.each_line do |line|
60
+ if line.match?(TAILWIND_SOURCES_NEGATED_RE)
61
+ rule = :negate
62
+ elsif line.match?(TAILWIND_SOURCES_IGNORED_RE)
63
+ rule = :ignore
64
+ end
65
+ end
66
+ rule
67
+ end
24
68
 
25
69
  # Matches the version stamp the Dockerfile template writes, e.g.
26
70
  # `# docs-kit Dockerfile v1.0.2`. Absent on a hand-written Dockerfile a site
@@ -34,7 +78,7 @@ module DocsKit
34
78
  # The drift messages, in the order a site should act on them. Empty when
35
79
  # the site is clean.
36
80
  def items
37
- [render_page_drift, icon_helper_drift, dockerfile_drift].compact
81
+ [render_page_drift, icon_helper_drift, dockerfile_drift, tailwind_sources_drift].compact
38
82
  end
39
83
 
40
84
  def clean?
@@ -79,6 +123,48 @@ module DocsKit
79
123
  "diff against the template (bin/rails g docs_kit:install shows the path) and adopt the changes."
80
124
  end
81
125
 
126
+ # tailwind.sources.css is regenerated by bin/build-css with machine-local
127
+ # absolute gem paths — the generator gitignores it, but a copy committed
128
+ # before the ignore stays tracked (gitignore doesn't untrack). Untracking
129
+ # stages a deletion, so we hand the site the exact command instead of
130
+ # touching its index.
131
+ def tailwind_sources_drift
132
+ return unless tracked_by_git?(TAILWIND_SOURCES)
133
+ # git's own verdict, not the recognized-lines regex: git resolves the
134
+ # FULL pattern semantics (broad globs, ordering, nested .gitignores),
135
+ # so a dead `!` line before a broader ignore still warns, while a
136
+ # genuinely effective negation (the site's opt-out) stays quiet.
137
+ return unless ignored_by_git?(TAILWIND_SOURCES)
138
+
139
+ "#{TAILWIND_SOURCES} is generated by bin/build-css but tracked by git — " \
140
+ "run `git rm --cached #{TAILWIND_SOURCES}` and commit (the ignore entry is in place)."
141
+ end
142
+
143
+ # True when the repo's own .gitignore rules cover `rel` — git's full
144
+ # pattern semantics (broad globs, `!` ordering, nested .gitignores), but
145
+ # SCOPED to the repo's committed convention: `--exclude-per-directory`
146
+ # consults only the .gitignore files, never `.git/info/exclude` or a
147
+ # global core.excludesFile, so the verdict (and the "the ignore entry is
148
+ # in place" guidance it backs) is identical on every machine.
149
+ # `--cached --ignored` reports a TRACKED path matching an exclude — the
150
+ # exact state this drift check exists to catch. Only called after
151
+ # tracked_by_git? proved git + a repo exist.
152
+ def ignored_by_git?(rel)
153
+ out = IO.popen(["git", "-C", @root, "ls-files", "--cached", "--ignored",
154
+ "--exclude-per-directory=.gitignore", "--", rel],
155
+ err: File::NULL, &:read)
156
+ !out.strip.empty?
157
+ end
158
+
159
+ # True when the site's git index tracks `rel`. Conservative: no git on
160
+ # PATH, not a repo, or an untracked file all read as "no drift". ls-files
161
+ # only consults the index (no commit needed), and `git -C` resolves the
162
+ # repo upward, so a docs site living in a subdir of a larger repo works.
163
+ def tracked_by_git?(rel)
164
+ system("git", "-C", @root, "ls-files", "--error-unmatch", rel,
165
+ out: File::NULL, err: File::NULL) == true
166
+ end
167
+
82
168
  def read(rel)
83
169
  path = File.join(@root, rel)
84
170
  File.exist?(path) ? File.read(path) : nil
@@ -15,7 +15,7 @@ FROM docker.io/library/ruby:$RUBY_VERSION-slim AS base
15
15
  # Rails app lives here.
16
16
  WORKDIR /rails
17
17
 
18
- ARG BUN_VERSION=1.3.2
18
+ ARG BUN_VERSION=1.4.2
19
19
  ENV BUN_INSTALL="/usr/local/bun" \
20
20
  PATH="/usr/local/bun/bin:$PATH" \
21
21
  BUNDLE_DEPLOYMENT="1" \
@@ -66,6 +66,9 @@ end
66
66
  - **The primary argument is positional; modifiers are keywords.**
67
67
  `Section("Title", description:)`, `Code(source, filename:)`,
68
68
  `Header("Title", eyebrow:)`.
69
+ - **`Code`'s `filename:` selects the language** (`*.yml` → yaml, `Dockerfile`
70
+ → docker, `*.sh` → shell, …); pass `lexer:` only to override the guess or when
71
+ there is no filename (the default is ruby).
69
72
  - **Wrappers that take no positional arg use lowercase page helpers** so a block
70
73
  needs no parens: `md <<~'MD' … MD`, `prose { … }`, `example { |ex| … }`,
71
74
  `operation "operationId"`. (A bare `DocsUI::Prose do` is a Ruby SyntaxError; the
@@ -101,13 +101,19 @@ module DocsKit
101
101
  source.match?(/^\s*entries\s*\[/) && !source.match?(/^\s*page\s+["']/)
102
102
  end
103
103
 
104
- # Inject after the last existing `page` line so ordering lands at the end
105
- # of the group; else after view_namespace/path_prefix; else after the
106
- # `extend DocsKit::Registry` line.
104
+ # Inject after the last existing `page` line of the FILE so ordering lands
105
+ # at the end of the last group; else after view_namespace/path_prefix;
106
+ # else after the `extend DocsKit::Registry` line.
107
+ #
108
+ # The `page` anchor is the last matching line as a String, not a Regexp:
109
+ # Thor's inject_into_file replaces EVERY match of a Regexp `after:`, so a
110
+ # "page line not followed by a page line" pattern fires once per group in
111
+ # a registry laid out with blank-line-separated groups (the layout
112
+ # docs_kit:install produces) and duplicates the entry.
107
113
  def registry_anchor(source)
108
114
  case source
109
115
  when /^\s*page\s+["']/
110
- /^\s*page .*\n(?!\s*page )/
116
+ source.lines.grep(/^\s*page\s+["']/).last
111
117
  when /^\s*view_namespace\s/
112
118
  /^\s*view_namespace .*\n/
113
119
  when /^\s*path_prefix\s/
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: docs-kit
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.1
4
+ version: 1.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mikael Henriksson
@@ -18,7 +18,7 @@ dependencies:
18
18
  version: '1.2'
19
19
  - - "<"
20
20
  - !ruby/object:Gem::Version
21
- version: '2'
21
+ version: '3'
22
22
  type: :runtime
23
23
  prerelease: false
24
24
  version_requirements: !ruby/object:Gem::Requirement
@@ -28,7 +28,7 @@ dependencies:
28
28
  version: '1.2'
29
29
  - - "<"
30
30
  - !ruby/object:Gem::Version
31
- version: '2'
31
+ version: '3'
32
32
  - !ruby/object:Gem::Dependency
33
33
  name: phlex-rails
34
34
  requirement: !ruby/object:Gem::Requirement
@@ -244,6 +244,7 @@ files:
244
244
  - lib/generators/docs_kit/install/install_generator.rb
245
245
  - lib/generators/docs_kit/install/migration.rb
246
246
  - lib/generators/docs_kit/install/migration_registry.rb
247
+ - lib/generators/docs_kit/install/migrations/v1_2_0.rb
247
248
  - lib/generators/docs_kit/install/sync_report.rb
248
249
  - lib/generators/docs_kit/install/templates/Dockerfile.tt
249
250
  - lib/generators/docs_kit/install/templates/agents_md.erb
@@ -281,7 +282,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
281
282
  requirements:
282
283
  - - ">="
283
284
  - !ruby/object:Gem::Version
284
- version: '3.2'
285
+ version: '3.3'
285
286
  required_rubygems_version: !ruby/object:Gem::Requirement
286
287
  requirements:
287
288
  - - ">="