automatic 14.12.2 → 26.09

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.
Files changed (177) hide show
  1. checksums.yaml +5 -5
  2. data/README.md +669 -83
  3. data/VERSION +1 -1
  4. data/assets/siteinfo/items_all.json +60300 -52138
  5. data/automatic.gemspec +110 -248
  6. data/bin/automatic +20 -139
  7. data/config/feed2console.yml +10 -4
  8. data/config/feed2markdown.yml +41 -0
  9. data/doc/AI_TUTORIAL.md +542 -0
  10. data/doc/BASIC_DESIGN.md +547 -0
  11. data/doc/COPYING.LESSER +165 -0
  12. data/doc/DEPLOYMENT.md +830 -0
  13. data/doc/LICENSE.md +14 -0
  14. data/doc/PLUGINS.md +2015 -0
  15. data/doc/PLUGIN_DEVELOPMENT.md +86 -0
  16. data/doc/POLICY.md +962 -0
  17. data/doc/QUICKSTART.md +260 -0
  18. data/doc/RELEASING.md +394 -0
  19. data/doc/REQUIREMENTS.md +556 -0
  20. data/doc/VERSIONS +266 -0
  21. data/lib/automatic/cli.rb +270 -0
  22. data/lib/automatic/environment.rb +31 -5
  23. data/lib/automatic/feed_maker.rb +65 -25
  24. data/lib/automatic/feed_parser.rb +51 -35
  25. data/lib/automatic/http.rb +107 -0
  26. data/lib/automatic/log.rb +49 -18
  27. data/lib/automatic/opml.rb +3 -1
  28. data/lib/automatic/pipeline.rb +73 -32
  29. data/lib/automatic/recipe.rb +100 -17
  30. data/lib/automatic/version.rb +14 -1
  31. data/lib/automatic.rb +93 -20
  32. data/plugins/custom_feed/svn_log.rb +73 -32
  33. data/plugins/custom_feed/web.rb +348 -0
  34. data/plugins/filter/absolute_uri.rb +43 -27
  35. data/plugins/filter/accept.rb +38 -45
  36. data/plugins/filter/batch.rb +97 -0
  37. data/plugins/filter/claude.rb +217 -0
  38. data/plugins/filter/clear.rb +12 -8
  39. data/plugins/filter/description_link.rb +66 -51
  40. data/plugins/filter/full_feed.rb +170 -52
  41. data/plugins/filter/gemini.rb +216 -0
  42. data/plugins/filter/github_feed.rb +38 -26
  43. data/plugins/filter/ignore.rb +33 -44
  44. data/plugins/filter/image.rb +36 -25
  45. data/plugins/filter/image_source.rb +75 -52
  46. data/plugins/filter/join.rb +105 -0
  47. data/plugins/filter/kimi.rb +216 -0
  48. data/plugins/filter/limit.rb +57 -0
  49. data/plugins/filter/one.rb +19 -26
  50. data/plugins/filter/open_ai.rb +198 -0
  51. data/plugins/filter/present.rb +75 -0
  52. data/plugins/filter/rand.rb +16 -17
  53. data/plugins/filter/sakura_ai.rb +205 -0
  54. data/plugins/filter/sanitize.rb +29 -34
  55. data/plugins/filter/sort.rb +20 -27
  56. data/plugins/filter/tumblr_resize.rb +31 -23
  57. data/plugins/notify/ikachan.rb +86 -48
  58. data/plugins/provide/fluentd.rb +43 -24
  59. data/plugins/publish/amazon_s3.rb +79 -40
  60. data/plugins/publish/console.rb +19 -16
  61. data/plugins/publish/console_link.rb +20 -17
  62. data/plugins/publish/eject.rb +48 -26
  63. data/plugins/publish/fluentd.rb +50 -30
  64. data/plugins/publish/hatena_bookmark.rb +88 -71
  65. data/plugins/publish/instapaper.rb +69 -59
  66. data/plugins/publish/markdown.rb +278 -0
  67. data/plugins/publish/memcached.rb +35 -29
  68. data/plugins/store/database.rb +50 -48
  69. data/plugins/store/digest.rb +212 -0
  70. data/plugins/store/file.rb +107 -68
  71. data/plugins/store/full_text.rb +31 -28
  72. data/plugins/store/permalink.rb +18 -22
  73. data/plugins/subscription/feed.rb +34 -21
  74. data/plugins/subscription/link.rb +31 -32
  75. data/plugins/subscription/text.rb +40 -45
  76. data/plugins/subscription/tumblr.rb +55 -44
  77. data/plugins/subscription/xml.rb +40 -36
  78. metadata +112 -321
  79. data/Gemfile +0 -38
  80. data/Rakefile +0 -59
  81. data/doc/ChangeLog +0 -303
  82. data/doc/PLUGINS +0 -750
  83. data/doc/PLUGINS.ja +0 -753
  84. data/doc/README +0 -511
  85. data/doc/README.ja +0 -519
  86. data/plugins/filter/google_news.rb +0 -50
  87. data/plugins/publish/google_calendar.rb +0 -84
  88. data/plugins/publish/hipchat.rb +0 -46
  89. data/plugins/publish/pocket.rb +0 -45
  90. data/plugins/publish/twitter.rb +0 -58
  91. data/plugins/subscription/chan_toru.rb +0 -57
  92. data/plugins/subscription/g_guide.rb +0 -57
  93. data/plugins/subscription/pocket.rb +0 -51
  94. data/plugins/subscription/twitter.rb +0 -69
  95. data/plugins/subscription/twitter_search.rb +0 -50
  96. data/plugins/subscription/weather.rb +0 -33
  97. data/script/build +0 -84
  98. data/spec/fixtures/sampleFeeds.tsv +0 -1
  99. data/spec/fixtures/sampleFeeds2.tsv +0 -2
  100. data/spec/fixtures/sampleRecipe.yml +0 -24
  101. data/spec/lib/automatic/log_spec.rb +0 -32
  102. data/spec/lib/automatic/pipeline_spec.rb +0 -68
  103. data/spec/lib/automatic/recipe_spec.rb +0 -40
  104. data/spec/lib/automatic_spec.rb +0 -99
  105. data/spec/plugins/custom_feed/svn_log_spec.rb +0 -31
  106. data/spec/plugins/filter/absolute_uri_spec.rb +0 -61
  107. data/spec/plugins/filter/accept_spec.rb +0 -331
  108. data/spec/plugins/filter/clear_spec.rb +0 -49
  109. data/spec/plugins/filter/description_link_spec.rb +0 -138
  110. data/spec/plugins/filter/full_feed_spec.rb +0 -129
  111. data/spec/plugins/filter/github_feed_spec.rb +0 -55
  112. data/spec/plugins/filter/google_news_spec.rb +0 -69
  113. data/spec/plugins/filter/ignore_spec.rb +0 -328
  114. data/spec/plugins/filter/image_source_spec.rb +0 -89
  115. data/spec/plugins/filter/image_spec.rb +0 -65
  116. data/spec/plugins/filter/one_spec.rb +0 -71
  117. data/spec/plugins/filter/rand_spec.rb +0 -52
  118. data/spec/plugins/filter/sanitize_spec.rb +0 -153
  119. data/spec/plugins/filter/sort_spec.rb +0 -189
  120. data/spec/plugins/filter/tumblr_resize_spec.rb +0 -109
  121. data/spec/plugins/notify/ikachan_spec.rb +0 -58
  122. data/spec/plugins/provide/fluentd_spec.rb +0 -49
  123. data/spec/plugins/publish/amazon_s3_spec.rb +0 -40
  124. data/spec/plugins/publish/console_spec.rb +0 -30
  125. data/spec/plugins/publish/eject_spec.rb +0 -40
  126. data/spec/plugins/publish/fluentd_spec.rb +0 -40
  127. data/spec/plugins/publish/google_calendar_spec.rb +0 -83
  128. data/spec/plugins/publish/hatena_bookmark_spec.rb +0 -134
  129. data/spec/plugins/publish/hipchat_spec.rb +0 -69
  130. data/spec/plugins/publish/instapaper_spec.rb +0 -82
  131. data/spec/plugins/publish/memcached_spec.rb +0 -63
  132. data/spec/plugins/publish/pocket_spec.rb +0 -51
  133. data/spec/plugins/publish/twitter_spec.rb +0 -73
  134. data/spec/plugins/store/file_spec.rb +0 -58
  135. data/spec/plugins/store/full_text_spec.rb +0 -152
  136. data/spec/plugins/store/permalink_spec.rb +0 -206
  137. data/spec/plugins/subscription/chan_toru_spec.rb +0 -56
  138. data/spec/plugins/subscription/feed_spec.rb +0 -71
  139. data/spec/plugins/subscription/g_guide_spec.rb +0 -82
  140. data/spec/plugins/subscription/link_spec.rb +0 -72
  141. data/spec/plugins/subscription/pocket_spec.rb +0 -57
  142. data/spec/plugins/subscription/text_spec.rb +0 -84
  143. data/spec/plugins/subscription/tumblr_spec.rb +0 -74
  144. data/spec/plugins/subscription/twitter_search_spec.rb +0 -57
  145. data/spec/plugins/subscription/twitter_spec.rb +0 -73
  146. data/spec/plugins/subscription/weather_spec.rb +0 -44
  147. data/spec/plugins/subscription/xml_spec.rb +0 -84
  148. data/spec/spec_helper.rb +0 -106
  149. data/spec/user_dir/plugins/store/mock.rb +0 -16
  150. data/test/fixtures/sampleOPML.xml +0 -11
  151. data/test/integration/test_absoluteurl.yml +0 -25
  152. data/test/integration/test_activerecord.yml +0 -24
  153. data/test/integration/test_add_pocket.yml +0 -26
  154. data/test/integration/test_chan_toru.yml +0 -21
  155. data/test/integration/test_descriptionlink.yml +0 -21
  156. data/test/integration/test_fluentd.yml +0 -22
  157. data/test/integration/test_fulltext.yml +0 -30
  158. data/test/integration/test_google_news.yml +0 -21
  159. data/test/integration/test_googlealert.yml +0 -21
  160. data/test/integration/test_hatenabookmark.yml +0 -30
  161. data/test/integration/test_ignore.yml +0 -25
  162. data/test/integration/test_ignore2.yml +0 -22
  163. data/test/integration/test_image2local.yml +0 -33
  164. data/test/integration/test_instapaper.yml +0 -26
  165. data/test/integration/test_link2local.yml +0 -34
  166. data/test/integration/test_one.yml +0 -23
  167. data/test/integration/test_pocket.yml +0 -22
  168. data/test/integration/test_rand.yml +0 -21
  169. data/test/integration/test_sanitize.yml +0 -23
  170. data/test/integration/test_sort.yml +0 -36
  171. data/test/integration/test_svnlog.yml +0 -15
  172. data/test/integration/test_text2feed.yml +0 -36
  173. data/test/integration/test_tumblr2local.yml +0 -43
  174. data/test/integration/test_twitter_search.yml +0 -22
  175. data/test/integration/test_weather.yml +0 -19
  176. data/test/integration/test_xml2fluentd.yml +0 -21
  177. data/vendor/.gitkeep +0 -0
data/doc/QUICKSTART.md ADDED
@@ -0,0 +1,260 @@
1
+ # Quick Start
2
+
3
+ This guide takes the public pages listed in its Recipe through one short
4
+ Automatic Ruby pipeline and leaves what they publish as one Markdown document.
5
+ It needs no account, no credential, no paid service and no database server.
6
+
7
+ It does need optional gems because of the plugins the Recipe names, not because
8
+ of the framework. The exact requirements are derived in
9
+ [Install what the Recipe needs](#4-install-what-the-recipe-needs) from the
10
+ canonical optional-dependency table in
11
+ [`DEPLOYMENT.md`](DEPLOYMENT.md#optional-plugin-dependencies), rather than
12
+ being maintained as a separate count here. Automatic Ruby installs what the
13
+ framework needs and leaves a plugin's gems to the operator who uses that
14
+ plugin, so "which plugins does this Recipe name, and what do they need" is a
15
+ question every Recipe asks. Skipping it is the usual way a first run stops half
16
+ way through.
17
+
18
+ ## 1. Install
19
+
20
+ Use Ruby 3.3 through 4.0 on a Unix-like system:
21
+
22
+ ```sh
23
+ ruby -v
24
+ gem install automatic
25
+ automatic --version
26
+ ```
27
+
28
+ Working from a Git checkout instead is this same guide with one step done
29
+ differently; "From a source checkout" at the end is that step.
30
+
31
+ ## 2. Create the user directory
32
+
33
+ ```sh
34
+ automatic scaffold
35
+ ```
36
+
37
+ This creates `~/.automatic` and copies the shipped examples to
38
+ `~/.automatic/config/example`. Existing files and directories are not
39
+ overwritten.
40
+
41
+ ## 3. Write the Recipe
42
+
43
+ A Recipe is one job: the plugins it runs, in order, with their settings. This
44
+ one reads the public index pages listed under `sites` as HTML and makes a feed of the articles each
45
+ lists — which is what to do for a page whose feed you do not have — keeps a
46
+ record of what it has already seen, and appends the rest to a Markdown
47
+ document.
48
+
49
+ Write it to `~/.automatic/config/web2markdown.yml`:
50
+
51
+ ```yaml
52
+ plugins:
53
+ - module: CustomFeedWeb
54
+ config:
55
+ retry: 2
56
+ interval: 2
57
+ sites:
58
+ # Articles: https://blog.python.org/2026/08/python-3147-31315/
59
+ - name: Python Insider
60
+ url: https://blog.python.org/
61
+ link_selector: 'a[href]'
62
+ include:
63
+ - '^https://blog\.python\.org/20[0-9]{2}/[0-9]{2}/[^/]+/?$'
64
+ same_host: true
65
+ fetch_items: 20
66
+
67
+ # Articles: https://blog.rust-lang.org/2026/08/04/enabling-polonius-alpha-on-nightly/
68
+ - name: Rust Blog
69
+ url: https://blog.rust-lang.org/
70
+ link_selector: 'a[href]'
71
+ include:
72
+ - '^https://blog\.rust-lang\.org/20[0-9]{2}/[0-9]{2}/[0-9]{2}/[^/]+/?$'
73
+ same_host: true
74
+ fetch_items: 20
75
+
76
+ # Articles: https://go.dev/blog/pkgsite-api
77
+ - name: The Go Blog
78
+ url: https://go.dev/blog/
79
+ link_selector: 'a[href]'
80
+ include:
81
+ - '^https://go\.dev/blog/[^/]+$'
82
+ same_host: true
83
+ fetch_items: 20
84
+
85
+ # Alerts: https://www.jpcert.or.jp/at/2026/at260021.html
86
+ - name: JPCERT/CC Alerts
87
+ url: https://www.jpcert.or.jp/at/2026.html
88
+ link_selector: 'a[href]'
89
+ include:
90
+ - '^https://www\.jpcert\.or\.jp/at/20[0-9]{2}/at[0-9]+\.html$'
91
+ same_host: true
92
+ fetch_items: 20
93
+
94
+ - module: StoreDigest
95
+ config:
96
+ db: web2markdown.db
97
+ fields:
98
+ - title
99
+ - link
100
+
101
+ - module: PublishMarkdown
102
+ config:
103
+ file: ~/.automatic/markdown/web.md
104
+ mode: append
105
+ ```
106
+
107
+ Each plugin hands its result to the next:
108
+
109
+ - **`CustomFeedWeb`** fetches each page and makes a feed of the article links it
110
+ lists. `include` is what tells an article from a navigation link, `interval`
111
+ is the pause between requests, and one run makes one request per site —
112
+ nothing here follows a link or reads an article body.
113
+ - **`StoreDigest`** records a digest of each item and passes on only the items
114
+ whose digest it had not recorded already. It is what makes the Recipe safe to
115
+ run repeatedly. `db` is a **file name**, kept under `~/.automatic/db`.
116
+ - **`PublishMarkdown`** appends what is left to a plain-text document, creating
117
+ the directory if it is missing. Its `file`, unlike `db`, is a path.
118
+
119
+ ## 4. Install what the Recipe needs
120
+
121
+ Read the Recipe you have just written, plugin by plugin, and look each one up in
122
+ the table of optional plugin dependencies in [`DEPLOYMENT.md`](DEPLOYMENT.md).
123
+ For the plugins in this Recipe, that table resolves the requirements as
124
+ follows:
125
+
126
+ - **`CustomFeedWeb`** — `nokogiri`, which it reads the pages with. In a
127
+ checkout, the group `html`.
128
+ - **`StoreDigest`** — `activerecord` and `sqlite3`, which it keeps its record
129
+ in. In a checkout, the group `store`.
130
+ - **`PublishMarkdown`** — nothing of its own.
131
+
132
+ ```sh
133
+ gem install nokogiri
134
+ gem install activerecord sqlite3
135
+ ```
136
+
137
+ **Install what the whole Recipe needs, not what its first plugin needs.** A
138
+ plugin is loaded when the pipeline reaches it, so installing only `nokogiri`
139
+ would let `CustomFeedWeb` fetch its pages, hand its feeds on, and stop the run
140
+ where the next plugin is loaded:
141
+
142
+ ```text
143
+ automatic: The `activerecord` gem is not installed. It is needed by the store
144
+ plugins. Install it with `gem install activerecord`, or in a source checkout add
145
+ its group to the bundle; see the optional plugin dependencies in
146
+ doc/DEPLOYMENT.md. (cannot load such file -- active_record)
147
+ ```
148
+
149
+ `PublishMarkdown` is the entry worth reading twice: it needs no gem of its own,
150
+ using an HTML parser where one is installed and its own substitution where none
151
+ is. A plugin's row in that table is the answer, not a guess from what the
152
+ plugin does.
153
+
154
+ `nokogiri` and `sqlite3` build a native extension where no binary package
155
+ matches your platform. Install a build environment only if one of them says it
156
+ needs one.
157
+
158
+ ## 5. Run it
159
+
160
+ ```sh
161
+ automatic -c ~/.automatic/config/web2markdown.yml
162
+ ```
163
+
164
+ A bare name is resolved inside `~/.automatic/config`, so `automatic -c
165
+ web2markdown.yml` is the same command. The log names each page as it is fetched,
166
+ each digest as it is saved, and the document as it is written.
167
+
168
+ ## 6. Read the result
169
+
170
+ ```sh
171
+ sed -n '1,80p' ~/.automatic/markdown/web.md
172
+ ```
173
+
174
+ Each item is a level-2 heading followed by the metadata the feed carries:
175
+
176
+ ```markdown
177
+ ## Extending the pkgsite API
178
+
179
+ - Link: <https://go.dev/blog/pkgsite-api>
180
+ - Date: 2026-08-17 09:00:00 +0900
181
+ ```
182
+
183
+ ## 7. Run it again
184
+
185
+ ```sh
186
+ automatic -c ~/.automatic/config/web2markdown.yml
187
+ ```
188
+
189
+ The second run appends nothing: the pages still list the same articles, and
190
+ `StoreDigest` has the digest of every one of them. An article published between
191
+ the two runs is the one thing that would be added — which is what makes this
192
+ Recipe safe to put in `cron`, and what to check before any Recipe with an
193
+ effect.
194
+
195
+ If instead everything is appended a second time, the store plugin is writing
196
+ somewhere other than where you think. The `Using Database:` line of the log
197
+ names the file it opened.
198
+
199
+ ## 8. Run it from cron
200
+
201
+ Create the log directory once, then use the absolute path reported by
202
+ `command -v automatic`:
203
+
204
+ ```sh
205
+ mkdir -p ~/.automatic/log
206
+ command -v automatic
207
+ ```
208
+
209
+ ```crontab
210
+ 0 * * * * /usr/local/bin/automatic -c $HOME/.automatic/config/web2markdown.yml >> $HOME/.automatic/log/web2markdown.log 2>&1
211
+ ```
212
+
213
+ Automatic Ruby runs once and exits; `cron` supplies the schedule.
214
+
215
+ ## From a source checkout
216
+
217
+ The normal installation above remains the quickest way to use Automatic Ruby. To
218
+ try the development version or change the source, first follow
219
+ [README's checkout setup](../README.md#from-a-checkout). Every `automatic` above
220
+ then becomes `bundle exec bin/automatic`, run from the checkout directory:
221
+
222
+ ```sh
223
+ cd ~/automaticruby
224
+ bundle exec bin/automatic scaffold
225
+ bundle exec bin/automatic -c ~/.automatic/config/web2markdown.yml
226
+ ```
227
+
228
+ The "Install what the Recipe needs" step is the step that differs, because a
229
+ checkout resolves its gems through Bundler rather than through RubyGems. Each
230
+ optional gem is in a Bundler group, and the Recipe's groups — `html` and
231
+ `store`, from that step — are selected together and installed once:
232
+
233
+ ```sh
234
+ bundle config set --local with "html store"
235
+ bundle install
236
+ ```
237
+
238
+ `gem install nokogiri` does **not** work here: the gem installs, and the
239
+ checkout still reports it as missing, because it is not in the bundle. That
240
+ difference, the commands that show what the bundle holds, and the same Recipe
241
+ plugins taken step by step through choosing their groups are in
242
+ [`DEPLOYMENT.md`](DEPLOYMENT.md) under "Working out what a Recipe needs, in a
243
+ checkout".
244
+
245
+ ## Next
246
+
247
+ The Recipe is ordinary YAML. Add a site, change the Markdown path, or put
248
+ `PublishConsoleLink` at the end to see what the pipeline holds without writing
249
+ anything. The shipped `feed2markdown.yml` and `feed2console.yml` in
250
+ `~/.automatic/config/example` are the same shape over an ordinary feed, for a
251
+ site that publishes one.
252
+
253
+ Taking this same pipeline further — the article bodies, one joined text, and an
254
+ AI service asked one question about it — is
255
+ [`AI_TUTORIAL.md`](AI_TUTORIAL.md).
256
+
257
+ What each plugin does is [`PLUGINS.md`](PLUGINS.md) section 6; installing,
258
+ scheduling and operating a Recipe is [`DEPLOYMENT.md`](DEPLOYMENT.md); writing a
259
+ small plugin of your own is
260
+ [`PLUGIN_DEVELOPMENT.md`](PLUGIN_DEVELOPMENT.md).
data/doc/RELEASING.md ADDED
@@ -0,0 +1,394 @@
1
+ # Releasing Automatic Ruby
2
+
3
+ This is the maintainer runbook for building and publishing the `automatic` gem.
4
+ It describes a manual release. Building and inspecting a gem does not publish
5
+ it; `gem push` does, and must be run only for an explicitly approved release.
6
+
7
+ The governing principles are:
8
+
9
+ - Build locally.
10
+ - Verify before publishing.
11
+ - Keep credentials out of the repository.
12
+ - Publish only an explicitly approved release.
13
+
14
+ Do not restore the historical Jeweler tasks. `automatic.gemspec` is maintained
15
+ by hand, `Rakefile` contains test tasks only, and RubyGems builds the package
16
+ directly from the gemspec.
17
+
18
+ ## 1. Prerequisites
19
+
20
+ The releaser needs:
21
+
22
+ - a supported Ruby, currently Ruby 3.3 through 4.0;
23
+ - current Bundler and RubyGems versions compatible with that Ruby;
24
+ - write access to the source repository and permission to create and push tags;
25
+ - ownership of `automatic` on RubyGems.org, or an API key with push permission;
26
+ - access to the RubyGems.org account and its required MFA method.
27
+
28
+ Check the local tools before changing release metadata:
29
+
30
+ ```sh
31
+ ruby -v
32
+ gem --version
33
+ bundle --version
34
+ ```
35
+
36
+ The release Ruby need not reproduce the complete CI matrix locally. Required GitHub Actions checks must be green for every Ruby version in the
37
+ current matrix of [`.github/workflows/ci.yml`](../.github/workflows/ci.yml)
38
+ before publication.
39
+
40
+ ## 2. Authentication and credentials
41
+
42
+ RubyGems authenticates publishing operations with an API key. Confirm the
43
+ account is an owner of the package before release; gem ownership grants the
44
+ ability to push versions and yank releases. Use the least privilege needed for
45
+ the release and follow the current RubyGems.org account and API-key guidance.
46
+
47
+ The standard RubyGems sign-in command stores a key in the RubyGems credentials
48
+ file:
49
+
50
+ ```sh
51
+ gem signin
52
+ ```
53
+
54
+ RubyGems commonly uses `~/.gem/credentials`; with the XDG directory layout it
55
+ may report another user-specific path. Use the path reported by the installed
56
+ RubyGems. The file contains secrets and must be readable and writable only by
57
+ its owner:
58
+
59
+ ```sh
60
+ chmod 0600 ~/.gem/credentials
61
+ ```
62
+
63
+ Never put an API key, password, OTP or credentials file in this repository, the
64
+ Gemfile, the gemspec, a shell script, README, a commit or a GitHub artifact. Do
65
+ not show a real key in an example. Do not pass a key as a literal command-line
66
+ argument, because process listings and shell history may retain it.
67
+
68
+ The gemspec sets `rubygems_mfa_required` to `true`. If RubyGems.org requires
69
+ MFA, complete the interactive prompt from `gem signin` or `gem push`. RubyGems
70
+ also supports `--otp CODE` and the short-lived `GEM_HOST_OTP_CODE` environment
71
+ variable. Do not store an OTP in the repository or a persistent script.
72
+
73
+ Before the release window, verify access through the RubyGems.org web account.
74
+ Do not change owners or create, rotate or revoke production keys as an
75
+ incidental part of this runbook.
76
+
77
+ ## 3. Prepare the source version
78
+
79
+ Start from a clean checkout of the release branch and fetch the authoritative
80
+ remote. Review the complete release diff and the top entry in `doc/VERSIONS`.
81
+
82
+ Automatic Ruby uses a calendar version taken from the release date:
83
+
84
+ ```text
85
+ X.YY release in year X, month YY
86
+ X.YY.PATCH correction to an earlier release in the same month
87
+ ```
88
+
89
+ `PATCH` may correct an earlier release in the same month, but never on the same
90
+ calendar date as that release. A versioned unit has one version number per
91
+ calendar date, without exception.
92
+
93
+ Section 10 of `POLICY.md` is authoritative. A release updates these release-metadata sources together in one
94
+ release-metadata commit:
95
+
96
+ - `VERSION` contains the package version;
97
+ - `lib/automatic/version.rb` defines `Automatic::VERSION` for the CLI;
98
+ - the current heading in `doc/VERSIONS` contains the same version.
99
+
100
+ `automatic.gemspec` does not contain another version literal. It reads and
101
+ strips the repository-root `VERSION` file. Confirm all three values agree:
102
+
103
+ ```sh
104
+ version=$(cat VERSION)
105
+ ruby -Ilib -e "require 'automatic/version'; abort unless Automatic::VERSION == '$version'"
106
+ ruby -e 'spec = Gem::Specification.load("automatic.gemspec")
107
+ abort unless spec.version.to_s == File.read("VERSION").strip'
108
+ ```
109
+
110
+ Finalize the current unreleased `doc/VERSIONS` entry as a release summary, not
111
+ a development diary. Change `(Release Date: TBD)` to the actual release date.
112
+ Do not add a bullet merely for finalizing the date. Remove or consolidate an
113
+ unreleased bullet only when it does not describe the source being published.
114
+
115
+ ## 4. Run release checks
116
+
117
+ Install the default development bundle and run the same default suite used by
118
+ the repository:
119
+
120
+ ```sh
121
+ bundle install
122
+ bundle exec rake
123
+ bundle exec ruby -Ilib -e "require 'automatic'"
124
+ bundle exec bin/automatic --version
125
+ bundle exec bin/automatic --help
126
+ ```
127
+
128
+ Also confirm that all required GitHub Actions checks are green for the release
129
+ commit. Do not publish from an uncommitted or dirty tree:
130
+
131
+ ```sh
132
+ git status --short
133
+ ```
134
+
135
+ ## 5. Build the gem
136
+
137
+ Build from the hand-maintained gemspec at the repository root:
138
+
139
+ ```sh
140
+ gem build automatic.gemspec
141
+ ```
142
+
143
+ This creates `automatic-X.Y.Z.gem` in the current directory. Here and below,
144
+ `X.Y.Z` means the exact value in `VERSION`; the project version may have two or
145
+ three components. The generated archive is ignored by Git and is not source.
146
+ Never commit it.
147
+
148
+ For copy-and-paste-safe inspection commands, capture its exact name:
149
+
150
+ ```sh
151
+ version=$(cat VERSION)
152
+ package="automatic-${version}.gem"
153
+ test -f "$package"
154
+ ```
155
+
156
+ ## 6. Inspect package contents
157
+
158
+ First inspect the archive's recorded file list without installing it:
159
+
160
+ ```sh
161
+ gem specification "$package" files
162
+ ```
163
+
164
+ For a filesystem view, unpack it into a fresh temporary directory:
165
+
166
+ ```sh
167
+ inspect_dir=$(mktemp -d)
168
+ gem unpack "$package" --target "$inspect_dir"
169
+ find "$inspect_dir/automatic-$version" -type f | sort
170
+ ```
171
+
172
+ Confirm that the package contains, at minimum:
173
+
174
+ - `lib/` and `lib/automatic/version.rb`;
175
+ - `bin/automatic`, recorded as the `automatic` executable;
176
+ - the maintained files under `plugins/`;
177
+ - the shipped configuration and assets;
178
+ - maintained documentation, including `README.md` and `doc/RELEASING.md`;
179
+ - `doc/LICENSE.md`, `doc/COPYING` and `doc/COPYING.LESSER`;
180
+ - `VERSION` and `automatic.gemspec`.
181
+
182
+ Confirm that it does not contain:
183
+
184
+ - a generated `.gem` archive or `pkg/` output;
185
+ - `.git`, `.github`, `.bundle`, `Gemfile.lock` or local Bundler directories;
186
+ - `Gemfile`, `Rakefile`, `script/`, `spec/`, `test/` or `vendor/`;
187
+ - credentials, databases, editor files, temporary files or coverage output;
188
+ - documents deleted or superseded in the maintained source tree.
189
+
190
+ The gemspec deliberately derives its list from files visible to Git, then
191
+ filters development and generated paths. Inspect every release archive anyway:
192
+ the build result, rather than the intended filter, is what would be published.
193
+
194
+ ## 7. Inspect package metadata
195
+
196
+ Read metadata from the built archive, not only from the source gemspec:
197
+
198
+ ```sh
199
+ gem specification "$package" name
200
+ gem specification "$package" version
201
+ gem specification "$package" summary
202
+ gem specification "$package" homepage
203
+ gem specification "$package" metadata
204
+ gem specification "$package" required_ruby_version
205
+ gem specification "$package" licenses
206
+ gem specification "$package" authors
207
+ gem specification "$package" dependencies
208
+ ```
209
+
210
+ Confirm that:
211
+
212
+ - the name is `automatic` and the version equals `VERSION`;
213
+ - the summary and homepage describe this project;
214
+ - `source_code_uri` is the Automatic Ruby repository;
215
+ - the required Ruby version matches `automatic.gemspec`;
216
+ - licenses contain both `GPL-3.0-only` and `LGPL-3.0-only`;
217
+ - the authors are correct;
218
+ - runtime and development dependencies match `automatic.gemspec`, and the
219
+ runtime ones are the framework's own — no gem that belongs to a plugin has
220
+ found its way in ([`POLICY.md`](POLICY.md) section 9.1);
221
+ - `rubygems_mfa_required` is `true`.
222
+
223
+ ## 8. Test an isolated local installation
224
+
225
+ Install the archive into a temporary gem home so the smoke test does not load
226
+ the checkout's `lib/` directory. Dependency downloads may require network
227
+ access if they are not already cached.
228
+
229
+ ```sh
230
+ gem_home=$(mktemp -d)
231
+ env -u RUBYLIB GEM_HOME="$gem_home" GEM_PATH="$gem_home" \
232
+ gem install "./$package" --no-document
233
+ env -u RUBYLIB GEM_HOME="$gem_home" GEM_PATH="$gem_home" \
234
+ "$gem_home/bin/automatic" --version
235
+ env -u RUBYLIB GEM_HOME="$gem_home" GEM_PATH="$gem_home" \
236
+ "$gem_home/bin/automatic" --help
237
+ ```
238
+
239
+ Confirm the installed version equals `VERSION`. Then run a Recipe that needs no
240
+ network and no optional gem, with a temporary home:
241
+
242
+ ```sh
243
+ smoke_home=$(mktemp -d)
244
+ mkdir -p "$smoke_home/.automatic/config"
245
+ cat >"$smoke_home/.automatic/config/smoke.yml" <<'YAML'
246
+ plugins:
247
+ - module: SubscriptionText
248
+ config:
249
+ feeds:
250
+ - title: release smoke test
251
+ url: https://example.com/
252
+ - module: PublishConsoleLink
253
+ YAML
254
+ env -u RUBYLIB HOME="$smoke_home" GEM_HOME="$gem_home" GEM_PATH="$gem_home" \
255
+ "$gem_home/bin/automatic" -c smoke.yml
256
+ ```
257
+
258
+ The command must print the example link without loading a file from the source
259
+ checkout. Remove the temporary directories when inspection is complete:
260
+
261
+ ```sh
262
+ rm -rf "$inspect_dir" "$gem_home" "$smoke_home"
263
+ ```
264
+
265
+ ## 9. Prepare the release commit and tag
266
+
267
+ After tests and package checks pass, commit the finalized release metadata.
268
+ Wait for required CI on that exact commit. Create an annotated tag named with
269
+ the version prefixed by `v`, as required by the existing version policy:
270
+
271
+ ```sh
272
+ version=$(cat VERSION)
273
+ git tag -a "v$version" -m "Release $version"
274
+ git push origin HEAD
275
+ git push origin "v$version"
276
+ ```
277
+
278
+ The tag identifies the source used to build the gem. Do not move or force-update
279
+ a release tag after publication. If the tag is wrong before publication, stop
280
+ and correct it openly. If publication has happened, preserve the source history
281
+ and prepare the correction in a new commit. Publish the correction under a new
282
+ version only on a later calendar date; do not create or publish a second
283
+ version on the same date.
284
+
285
+ The repository has no established requirement for a GitHub Release separate
286
+ from its Git tag. If maintainers create one, create it from the same immutable
287
+ tag and use the finalized `doc/VERSIONS` entry as its notes. The tag records the
288
+ source; the RubyGems version records the published package. Neither replaces
289
+ the other.
290
+
291
+ ## 10. Publish to RubyGems.org
292
+
293
+ Reconfirm approval, the package name and the version immediately before this
294
+ step. The following command performs the real publication to RubyGems.org:
295
+
296
+ ```sh
297
+ gem push "automatic-X.Y.Z.gem"
298
+ ```
299
+
300
+ Replace the placeholder with the archive already inspected. **Running this
301
+ command actually publishes the gem.** Do not run it for a dry run, a test, or
302
+ merely because the build succeeded. Complete any MFA prompt using the approved
303
+ RubyGems.org account.
304
+
305
+ RubyGems.org does not provide an overwrite operation for an existing name,
306
+ version and platform. A published version is immutable: never rebuild the same
307
+ version and expect a second push to replace it.
308
+
309
+ ## 11. Verify publication
310
+
311
+ Open <https://rubygems.org/gems/automatic> and confirm the new version appears.
312
+ Verify its version, both licenses, required Ruby version, dependency metadata,
313
+ homepage and source link against the archive inspected above.
314
+
315
+ Allow for normal index propagation, then install from RubyGems.org into another
316
+ fresh gem home rather than reusing the local-package test:
317
+
318
+ ```sh
319
+ verify_home=$(mktemp -d)
320
+ version=X.Y.Z
321
+ GEM_HOME="$verify_home" GEM_PATH="$verify_home" \
322
+ gem install automatic -v "$version" --no-document
323
+ GEM_HOME="$verify_home" GEM_PATH="$verify_home" \
324
+ "$verify_home/bin/automatic" --version
325
+ GEM_HOME="$verify_home" GEM_PATH="$verify_home" \
326
+ "$verify_home/bin/automatic" --help
327
+ rm -rf "$verify_home"
328
+ ```
329
+
330
+ The installed version must equal the tag, `VERSION`, `Automatic::VERSION`, the
331
+ `doc/VERSIONS` heading and the version shown by RubyGems.org.
332
+
333
+ ## 12. Publication failures and corrections
334
+
335
+ Stop on an error and diagnose it; do not bypass RubyGems security controls.
336
+
337
+ - **Authentication failure:** run `gem signin` for the correct account, check
338
+ the credentials path and permissions, and verify that the key is active and
339
+ has push scope. Never paste the key into source or a committed script.
340
+ - **MFA failure:** use the current OTP for the account and retry only the failed
341
+ push. Check the account's MFA configuration rather than disabling MFA.
342
+ - **Ownership failure:** have an existing owner grant the intended account
343
+ ownership or an appropriately scoped key. Do not publish under another name.
344
+ - **Version already exists:** do not attempt to overwrite it. Determine whether
345
+ it was already published correctly; otherwise fix the source, increment the
346
+ version according to `POLICY.md`, rebuild and repeat every verification step.
347
+ The incremented version is published on a later calendar date than the
348
+ existing one, never on the same date.
349
+ - **Malformed metadata or rejected gem:** fix the gemspec and release metadata,
350
+ increment the version if that version reached RubyGems.org, then rebuild,
351
+ inspect and test the new archive. Do not use `--force` to hide validation.
352
+ A version that reached RubyGems.org counts as that date's release, so the
353
+ incremented version waits for a later calendar date.
354
+
355
+ `gem yank automatic -v X.Y.Z` removes a published version from normal index use.
356
+ Yank only for an exceptional serious mispublication, such as exposed secrets or
357
+ a release that must not be installed. It is not the normal correction process,
358
+ does not make the version reusable, and does not erase every downloaded copy.
359
+ The normal response is to correct the problem, bump the version and publish a
360
+ new release on a later calendar date. Yanking does not permit a second version
361
+ on the date of the yanked release. Rotate any exposed secret immediately as
362
+ well as yanking.
363
+
364
+ Publication state and Git history are separate. Never rewrite a published
365
+ commit, force-push the release branch, or move a release tag to simulate a
366
+ rollback. Preserve a traceable record and publish a corrected version.
367
+
368
+ Future automation may use a publishing mechanism currently supported by
369
+ RubyGems.org, but introducing automatic publication or trusted publishing is a
370
+ separate, explicitly reviewed change.
371
+
372
+ ## Official RubyGems references
373
+
374
+ Recheck these official references when preparing a release, because account and
375
+ security behavior can change independently of this repository:
376
+
377
+ - [Publishing your gem](https://guides.rubygems.org/publishing/)
378
+ - [RubyGems command reference](https://guides.rubygems.org/command-reference/)
379
+ - [API key scopes](https://guides.rubygems.org/api-key-scopes/)
380
+ - [MFA requirement opt-in](https://guides.rubygems.org/mfa-requirement-opt-in/)
381
+ - [Removing a published gem](https://guides.rubygems.org/removing-a-published-gem/)
382
+
383
+ ## Quick checklist
384
+
385
+ - [ ] `VERSION`, `Automatic::VERSION` and the `doc/VERSIONS` heading agree.
386
+ - [ ] The release date and release summary are finalized.
387
+ - [ ] The working tree is clean and required CI is green.
388
+ - [ ] `bundle exec rake` passes and the gem builds.
389
+ - [ ] Package contents and metadata have been inspected.
390
+ - [ ] GPLv3/LGPLv3, Ruby version and dependencies are correct.
391
+ - [ ] The isolated local install and offline smoke test pass.
392
+ - [ ] The release commit and immutable `vX.Y.Z` tag identify the built source.
393
+ - [ ] Publication has explicit approval and `gem push` succeeds.
394
+ - [ ] RubyGems.org metadata and a clean remote install are verified.