automatic 14.12.2 → 26.08

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