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/DEPLOYMENT.md ADDED
@@ -0,0 +1,830 @@
1
+ # Deployment
2
+
3
+ How to install Automatic Ruby, set up a user directory, write and verify a
4
+ Recipe, put it in `cron`, and operate it afterwards.
5
+
6
+ What the system is belongs to [`REQUIREMENTS.md`](REQUIREMENTS.md); how a Recipe
7
+ is written belongs to [`PLUGINS.md`](PLUGINS.md). This document assumes you have
8
+ read neither and points at them where they are needed.
9
+
10
+ It stands on its own. Nothing in it is completed by a document kept in another
11
+ repository.
12
+
13
+ ## What this touches, and what it does not
14
+
15
+ Installing and running Automatic Ruby affects:
16
+
17
+ - the RubyGems installation you install into, or a checkout directory,
18
+ - `~/.automatic`, the user directory, and nothing else under your home
19
+ directory,
20
+ - whatever a Recipe of yours tells a plugin to write: files under a path you
21
+ name, rows in a SQLite database you name, requests to services you configure.
22
+
23
+ It does not install a service, does not write outside those places, does not
24
+ require root, and does not run as a daemon. There is nothing to start and
25
+ nothing to stop.
26
+
27
+ ## Before you begin
28
+
29
+ - A Unix-like system. GNU/Linux and macOS are what this is used on; Windows is
30
+ not supported.
31
+ - **Ruby 3.3 through 4.0.** Check with `ruby -v`. The continuously validated
32
+ versions are the matrix in [`.github/workflows/ci.yml`](../.github/workflows/ci.yml);
33
+ a supported version absent from that matrix is simply not checked on every
34
+ commit, and a Ruby newer than the matrix is permitted rather than refused.
35
+ See [`REQUIREMENTS.md`](REQUIREMENTS.md) section 20.
36
+ - Optional gems for particular plugins, listed in the table under
37
+ "Optional plugin dependencies" below. None is needed to install Automatic
38
+ Ruby, or to run a Recipe that does not use the plugin; a Recipe that does
39
+ name one installs it as a step of its own, which is what the "Install what
40
+ the Recipe needs" section of [`QUICKSTART.md`](QUICKSTART.md) demonstrates.
41
+ - A build environment may be needed for one of those optional gems — `nokogiri`
42
+ and `sqlite3` build a native extension where no binary package matches your
43
+ platform. The framework's own dependencies are pure Ruby, so the normal
44
+ installation needs no build tools; install them only if installing an
45
+ optional gem reports that they are required.
46
+
47
+ ## Install
48
+
49
+ ### From RubyGems
50
+
51
+ ```sh
52
+ gem install automatic
53
+ automatic --version
54
+ ```
55
+
56
+ That installs the framework, the `automatic` command and the runtime
57
+ dependencies declared in `automatic.gemspec`. That gemspec is the source of
58
+ truth for the framework's runtime dependency set. No HTML parser, no database, no service client — a gem needed by
59
+ one plugin is installed by the operator who uses that plugin, so installing
60
+ Automatic Ruby does not install what your Recipes do not use.
61
+
62
+ Add one when you use the plugin that needs it:
63
+
64
+ ```sh
65
+ gem install nokogiri # the plugins that read HTML
66
+ gem install activerecord sqlite3 # the store plugins
67
+ ```
68
+
69
+ The table under "Optional plugin dependencies" below says which plugin needs
70
+ which, and is the list to check before adding anything.
71
+
72
+ ### From a checkout
73
+
74
+ For working on the framework, or for running a version that is not released.
75
+ The supported checkout setups are shown below; start with the minimal setup.
76
+
77
+ **Minimal — the framework and its test suite.** What you want for running the
78
+ checkout, and for developing the framework itself:
79
+
80
+ ```sh
81
+ git clone https://github.com/id774/automaticruby.git
82
+ cd automaticruby
83
+ bundle install
84
+ bundle exec bin/automatic --version
85
+ bundle exec rake
86
+ ```
87
+
88
+ `bundle install` resolves the runtime dependencies of `automatic.gemspec` and
89
+ the development ones — `rake`, `rspec` and `simplecov`. It installs **no**
90
+ optional plugin gem: the `Gemfile`'s groups for those are optional, and Bundler
91
+ does not install an optional group unless it is asked to. If the `bundle`
92
+ command is unavailable, install Bundler first with `gem install bundler`.
93
+
94
+ **All supported optional plugin dependencies.** For plugin development, or for
95
+ running the specs of the plugins that need a gem:
96
+
97
+ ```sh
98
+ bundle config set --local with plugins
99
+ bundle install
100
+ bundle exec rake
101
+ ```
102
+
103
+ That installs the gems declared in the `plugins` group of `Gemfile`, and
104
+ their specs then run as part of the ordinary suite. The
105
+ [Optional plugin dependencies](#optional-plugin-dependencies) table maps
106
+ those dependencies to the plugins and tools that use them. The setting
107
+ is written to the checkout's own `.bundle/config`, which is not committed;
108
+ `bundle config unset --local with` returns the checkout to the minimum, and
109
+ `bundle install` afterwards.
110
+
111
+ **One dependency at a time.** Start minimal and add only what a plugin you
112
+ actually use needs. Each optional gem is in a second, smaller group named after
113
+ what it is for, so the group name selects it on its own:
114
+
115
+ ```sh
116
+ bundle config set --local with store # activerecord and sqlite3
117
+ bundle install
118
+ ```
119
+
120
+ Several at once are space-separated: `bundle config set --local with "store
121
+ html"`, and a Recipe using plugins from multiple groups needs exactly that. The group
122
+ names are in the table below, and "Working out what a Recipe needs, in a
123
+ checkout" takes one Recipe through choosing them.
124
+
125
+ In a checkout, `gem install <gem>` on its own is **not** enough: the checkout
126
+ resolves its gems through the bundle, so a gem the `Gemfile` does not mention is
127
+ not visible to it however plainly `gem list` shows it. Use the group, which is
128
+ why the groups exist. Outside a checkout — the installed gem, run as `automatic`
129
+ — there is no bundle and `gem install <gem>` is exactly right.
130
+
131
+ Everything below that says `automatic` becomes `bundle exec bin/automatic` in a
132
+ checkout.
133
+
134
+ ## Create the user directory
135
+
136
+ ```sh
137
+ automatic scaffold
138
+ ```
139
+
140
+ This creates `~/.automatic` and seeds it:
141
+
142
+ ```text
143
+ ~/.automatic/
144
+ ├── config/ your Recipes
145
+ │ └── example/ the Recipes shipped with the gem, copied here
146
+ ├── plugins/ your own plugins, in category subdirectories
147
+ ├── db/ SQLite databases the store plugins write
148
+ └── assets/ data files plugins read
149
+ └── siteinfo/
150
+ ```
151
+
152
+ It creates only what is missing, so running it again after an upgrade is safe
153
+ and will not overwrite a Recipe.
154
+
155
+ `automatic unscaffold` removes the whole directory — **including your Recipes
156
+ and your databases**. It is not an undo for `scaffold`.
157
+
158
+ The directory is optional. Where a part of it is absent the framework falls back
159
+ to the corresponding directory inside the installation, so a Recipe given by
160
+ full path runs without it.
161
+
162
+ ## Verify the installation
163
+
164
+ ```sh
165
+ automatic -c ~/.automatic/config/example/feed2console.yml
166
+ ```
167
+
168
+ That fetches one feed and prints its items. Distinguish an unreachable feed
169
+ from an optional-dependency failure:
170
+
171
+ - A message about the feed being unreachable means the network or that
172
+ particular feed, not the installation.
173
+ - A `LoadError` naming a gem means a plugin's optional dependency is not
174
+ installed. `feed2console.yml` uses none, so at this point it means the
175
+ installation itself; a message naming a gem and a plugin means the plugin,
176
+ and "Optional plugin dependencies" below says what to install.
177
+
178
+ To check the framework without any network at all, write a Recipe that uses
179
+ `SubscriptionText`:
180
+
181
+ ```yaml
182
+ # ~/.automatic/config/selftest.yml
183
+ global:
184
+ log:
185
+ level: info
186
+
187
+ plugins:
188
+ - module: SubscriptionText
189
+ config:
190
+ feeds:
191
+ - title: hello
192
+ url: https://example.com/
193
+ - module: PublishConsoleLink
194
+ ```
195
+
196
+ ```sh
197
+ automatic -c selftest.yml
198
+ ```
199
+
200
+ A bare name is resolved inside `~/.automatic/config`, which is why that command
201
+ has no path in it.
202
+
203
+ ## Write a Recipe
204
+
205
+ A Recipe is one job: the plugins it runs, in order, with their settings. The
206
+ format is specified in [`PLUGINS.md`](PLUGINS.md) section 2 and the plugins are
207
+ catalogued in section 6. A worked example, to save a blog's images:
208
+
209
+ ```yaml
210
+ # ~/.automatic/config/images.yml
211
+ global:
212
+ log:
213
+ level: info
214
+
215
+ plugins:
216
+ - module: SubscriptionFeed
217
+ config:
218
+ feeds:
219
+ - https://example.com/feed
220
+ retry: 3
221
+ interval: 5
222
+
223
+ - module: FilterImageSource
224
+
225
+ - module: StorePermalink
226
+ config:
227
+ db: images.db
228
+
229
+ - module: StoreFile
230
+ config:
231
+ path: /var/tmp/automatic/images
232
+ retry: 2
233
+ interval: 3
234
+ ```
235
+
236
+ That Recipe needs `nokogiri` for `FilterImageSource`, and `activerecord` plus
237
+ `sqlite3` for `StorePermalink`, because of the plugins it names rather than
238
+ because of the framework. The canonical mapping is the
239
+ [Optional plugin dependencies](#optional-plugin-dependencies) table.
240
+
241
+ The operational advice illustrated by that Recipe is:
242
+
243
+ - **`StorePermalink` before the plugin with the effect.** It records what has
244
+ been seen and passes on only what has not, which is what makes the Recipe safe
245
+ to run every hour. Without it, every run downloads everything again.
246
+ - **`interval` on anything that fetches repeatedly.** It is the pause between
247
+ requests, in seconds. Set it whenever a plugin will make more than a handful
248
+ of requests to one host.
249
+ - **`retry` on anything that reaches the network.** A transient failure retries
250
+ rather than ending the run.
251
+
252
+ ### Verify it before scheduling it
253
+
254
+ Run it by hand, more than once:
255
+
256
+ ```sh
257
+ automatic -c images.yml
258
+ echo "exit status: $?"
259
+ automatic -c images.yml # the second run should do much less
260
+ ```
261
+
262
+ The second run doing nothing is the store plugin working. If it does everything
263
+ again, the Recipe has no store plugin, or the database it names is not being
264
+ written where you think.
265
+
266
+ To see what a Recipe produces without any effect, replace the last plugin with
267
+ `PublishConsole` — which prints each item in full — or `PublishConsoleLink`,
268
+ which prints only links.
269
+
270
+ Diagnostic subcommands for the input end:
271
+
272
+ ```sh
273
+ automatic autodiscovery https://example.com/ # what feeds does this page advertise
274
+ automatic feedparser https://example.com/feed # does that feed parse
275
+ automatic inspect https://example.com/ # both, in one step
276
+ automatic opmlparser subscriptions.opml # the feed URLs in an OPML export
277
+ ```
278
+
279
+ ## Publishing to a Markdown document
280
+
281
+ A Recipe that ends in `PublishMarkdown` leaves what it collected as a Markdown
282
+ file, with no service and no credential involved. The plugin's specification —
283
+ what it writes for each field, and what it does with HTML in a body — is
284
+ [`PLUGINS.md`](PLUGINS.md) section 6.7. What matters when running it
285
+ unattended is below.
286
+
287
+ ```yaml
288
+ - module: StorePermalink
289
+ config:
290
+ db: feeds.db
291
+
292
+ - module: PublishMarkdown
293
+ config:
294
+ file: ~/notes/feeds.md
295
+ mode: append
296
+ ```
297
+
298
+ **The output path.** `file` is expanded, so `~` works, and a relative path is
299
+ resolved against the process's working directory — which under `cron` is your
300
+ home directory and is not worth relying on. Use an absolute path or a `~` path.
301
+ A missing parent directory is created; nothing else on the path is touched, and
302
+ the plugin writes to no other file.
303
+
304
+ **Append or overwrite.** `mode: append`, the default, adds the run's items to
305
+ the end of the file, which is what a Recipe in `cron` wants: with a store plugin
306
+ in front of it, each run contributes only what is new and the file becomes a
307
+ journal. `mode: overwrite` replaces the file, for a Recipe whose output is meant
308
+ to be the current state rather than a history — a digest regenerated every
309
+ morning, say. In either mode a run that produces no items writes nothing at all:
310
+ the file is not created, not appended to and not truncated, so a quiet run
311
+ leaves yesterday's document intact.
312
+
313
+ **Permissions.** The file is created with your umask, like any other file the
314
+ process writes. The plugin does not adjust it. Where the collected material is
315
+ private, put the document in a directory you have restricted rather than relying
316
+ on the file's own mode:
317
+
318
+ ```sh
319
+ mkdir -p ~/notes && chmod 700 ~/notes
320
+ ```
321
+
322
+ **Keeping the document clean.** With no `file`, the document goes to standard
323
+ output — and so does the log ([`REQUIREMENTS.md`](REQUIREMENTS.md) section 14),
324
+ so a redirect that collects one collects the other. Either disable log output in the Recipe or give `PublishMarkdown` a `file`:
325
+
326
+ ```yaml
327
+ global:
328
+ log:
329
+ level: none # standard output then carries the document alone
330
+ ```
331
+
332
+ ```sh
333
+ automatic -c feeds.yml > today.md
334
+ ```
335
+
336
+ or give the plugin a `file`, which leaves standard output to the log and is what
337
+ a `cron` entry wants, since the log is what you have afterwards when something
338
+ went wrong:
339
+
340
+ ```crontab
341
+ 0 7 * * * /usr/local/bin/automatic -c $HOME/.automatic/config/feeds.yml >> $HOME/.automatic/log/feeds.log 2>&1
342
+ ```
343
+
344
+ Redirecting the document itself from `cron` works as well, and then the log
345
+ needs somewhere else to go:
346
+
347
+ ```crontab
348
+ 0 7 * * * /usr/local/bin/automatic -c $HOME/.automatic/config/feeds.yml >> $HOME/notes/feeds.md 2>>$HOME/.automatic/log/feeds.log
349
+ ```
350
+
351
+ That entry is only clean if the Recipe sets `log.level: none`; otherwise the log
352
+ lines land in the document. Setting the level in the Recipe is the supported
353
+ way to do this, and the framework has no separate switch for it.
354
+
355
+ **Afterwards.** The file is ordinary text: read it, `grep` it, feed it to
356
+ another program, or keep it in a Git repository and commit after each run. The
357
+ plugin writes nothing that changes between runs of the same pipeline, so a
358
+ commit shows what arrived and nothing else.
359
+
360
+ ## Credentials
361
+
362
+ Plugins that reach an authenticated service take their credentials as ordinary
363
+ Recipe settings. **A Recipe holding a credential is a secret file**, and nothing
364
+ in the framework encrypts it or keeps it elsewhere:
365
+
366
+ ```sh
367
+ chmod 600 ~/.automatic/config/publish.yml
368
+ ```
369
+
370
+ Rules worth holding to:
371
+
372
+ - Keep credentials in their own Recipe rather than spread across several.
373
+ - Never commit a Recipe holding one, to this repository or to your own.
374
+ - The framework never logs a setting, and no plugin should log one. If you see a
375
+ credential in a log, that is a defect worth reporting.
376
+
377
+ This is a weakness inherited from the original design rather than a decision
378
+ made now; it is recorded in [`REQUIREMENTS.md`](REQUIREMENTS.md) section 17.
379
+
380
+ ## Schedule it
381
+
382
+ Automatic Ruby has no scheduler. One invocation runs one Recipe once and exits,
383
+ and `cron` decides when.
384
+
385
+ ```crontab
386
+ # m h dom mon dow command
387
+ 0 * * * * /usr/local/bin/automatic -c $HOME/.automatic/config/images.yml >> $HOME/.automatic/log/images.log 2>&1
388
+ ```
389
+
390
+ ```sh
391
+ mkdir -p ~/.automatic/log
392
+ ```
393
+
394
+ Points that matter in an unattended run:
395
+
396
+ - **Use an absolute path to the command.** `cron` has a short `PATH`. `command
397
+ -v automatic` gives you the path; in a checkout it is `bundle exec` from the
398
+ checkout directory, so use a small wrapper script rather than a long `cron`
399
+ line.
400
+ - **Capture both streams.** `2>&1` matters: the log goes to standard output and
401
+ errors go to standard error.
402
+ - **There is no locking.** A run that takes longer than its interval will
403
+ overlap with the next one. Either schedule with headroom, or wrap the command
404
+ in `flock`:
405
+
406
+ ```crontab
407
+ 0 * * * * /usr/bin/flock -n /tmp/automatic-images.lock /usr/local/bin/automatic -c $HOME/.automatic/config/images.yml >> $HOME/.automatic/log/images.log 2>&1
408
+ ```
409
+
410
+ - **Mind the service at the other end.** A Recipe reaching a rate-limited API is
411
+ scheduled with that limit in mind, and `interval` is set within the run.
412
+ - **The exit status is meaningful**: `0` the Recipe ran, `1` it failed, `2` the
413
+ command line was wrong. A monitoring system can use it.
414
+
415
+ ## Reading the log
416
+
417
+ One line per event, on standard output, through Ruby's `Logger`. The level is
418
+ set per Recipe:
419
+
420
+ ```yaml
421
+ global:
422
+ log:
423
+ level: info # info | warn | error | none
424
+ ```
425
+
426
+ - `info` — what each step did: the Recipe loaded, the feed parsed, the database
427
+ opened, the item stored. Verbose, and what you want while a Recipe is new.
428
+ - `warn` — what was skipped: an item missing a field, a save that failed.
429
+ - `error` — what failed: a fetch that exhausted its retries.
430
+ - `none` — nothing.
431
+
432
+ Once a Recipe is settled, `warn` keeps the log to what needs reading. Rotate the
433
+ file yourself; the framework does not.
434
+
435
+ ## Upgrading
436
+
437
+ ```sh
438
+ gem update automatic
439
+ automatic --version
440
+ automatic scaffold # adds anything new; overwrites nothing
441
+ ```
442
+
443
+ Then run each Recipe by hand once before trusting the schedule again. Your
444
+ Recipes and databases are in `~/.automatic` and are untouched by a gem upgrade.
445
+
446
+ Read [`VERSIONS`](VERSIONS) for what changed. A change that affects an existing
447
+ Recipe is stated there in terms you can act on.
448
+
449
+ ## Optional plugin dependencies
450
+
451
+ This table is the operator-facing source of truth for which plugin or tool
452
+ needs which optional dependency and how to install it. Current plugin support
453
+ status is maintained separately in
454
+ [`PLUGINS.md`](PLUGINS.md#6-the-plugins).
455
+
456
+ None of these gems is installed by `gem install automatic` or by a default
457
+ `bundle install`. Install one only if you use the plugin.
458
+
459
+ **Installed gem**: `gem install <gem>`. **Checkout**: `bundle config set
460
+ --local with <group>` and `bundle install`, because `bundle exec` sees only the
461
+ bundle. `plugins` is every group in the first block at once.
462
+
463
+ | Plugin | Needs | Installed gem | Checkout group |
464
+ | --- | --- | --- | --- |
465
+ | `StorePermalink`, `StoreFullText`, `StoreDigest` | `activerecord`, `sqlite3` | `gem install activerecord sqlite3` | `store` |
466
+ | `FilterImageSource`, `FilterDescriptionLink`, `SubscriptionLink`, `SubscriptionTumblr`, `CustomFeedWeb` | `nokogiri` | `gem install nokogiri` | `html` |
467
+ | `PublishMarkdown` | `nokogiri`, for HTML bodies only | `gem install nokogiri` | `html` |
468
+ | `FilterSanitize` | `sanitize` | `gem install sanitize` | `sanitize` |
469
+ | `autodiscovery` and `inspect` subcommands | `feedbag` | `gem install feedbag` | `autodiscovery` |
470
+ | `FilterFullFeed` | `nokogiri`, and a siteinfo file | `gem install nokogiri` | `html` |
471
+ | `CustomFeedSVNLog` | the `svn` command; no gem | — | — |
472
+ | `ProvideFluentd`, `PublishFluentd` | `fluent-logger`, and a Fluentd instance | `gem install fluent-logger` | `fluentd` |
473
+ | `PublishMemcached` | `dalli`, and a memcached server | `gem install dalli` | `memcached` |
474
+ | `PublishAmazonS3`, `StoreFile` S3 path | `aws-sdk-s3`, and a bucket | `gem install aws-sdk-s3` | `s3` |
475
+ | `PublishInstapaper` | an Instapaper account; no gem | — | — |
476
+ | `PublishEject` | the `eject` or `drutil` command | — | — |
477
+ | `NotifyIkachan` | an `ikachan` gateway you run | — | — |
478
+ | `FilterOpenAI`, `FilterClaude`, `FilterGemini`, `FilterSakuraAI`, `FilterKimi` | an account and an API token with that one service; no gem | — | — |
479
+ | `PublishHatenaBookmark` | the current Hatena API, which it does not speak | — | — |
480
+
481
+ Membership of the aggregate `plugins` group is defined in `Gemfile`. It
482
+ contains the optional gems whose corresponding specs need no external
483
+ service; the table above maps those dependencies to their consumers. The gems below it are in their own groups
484
+ only, because each of those plugins also needs a service, a bucket or a
485
+ command, and installing a gem alone would not make the plugin — or its spec —
486
+ work.
487
+
488
+ Both S3 rows make their requirement lazily, so `StoreFile`'s ordinary HTTP
489
+ download path and `PublishAmazonS3` in `mode: test` work with no AWS gem
490
+ installed at all. Leaving `access_key` and `secret_key` out of the Recipe is the
491
+ better way to use them: the SDK then takes credentials from the environment, a
492
+ shared profile or an instance role, and no long-lived secret sits in a file.
493
+
494
+ Two gems left this table in v26.08 and are not needed by anything now:
495
+ `xml-simple`, which `CustomFeedSVNLog` used to parse `svn log --xml` and which
496
+ REXML — already a dependency of the framework — parses instead, and `nkf`,
497
+ which `FilterDescriptionLink` used to normalize a page's encoding and which the
498
+ HTML parser does for itself. If you installed either for this project, nothing
499
+ here wants it any more.
500
+
501
+ No optional group is installed by default and none is installed in required CI,
502
+ so these plugins are outside what a green build guarantees. Installing a group
503
+ brings the specs of its plugins into the ordinary `bundle exec rake` run, which
504
+ is how they are verified.
505
+
506
+ Using a plugin without its gem is not a mystery: the plugin says what is
507
+ missing, what needs it and how to get it, and the command exits `1`.
508
+
509
+ ```text
510
+ automatic: The `activerecord` gem is not installed. It is needed by the store
511
+ plugins. Install it with `gem install activerecord`, ...
512
+ ```
513
+
514
+ ### Working out what a Recipe needs, in a checkout
515
+
516
+ The table says what a plugin needs. A Recipe needs the **sum** of what its
517
+ plugins need, worked out before the first run rather than discovered one failed
518
+ run at a time, and in a checkout that sum is a list of groups. The Quick Start's
519
+ Recipe — index pages watched for new articles, de-duplicated by content,
520
+ published as one Markdown document — is the worked example, shortened here to
521
+ one page:
522
+
523
+ ```yaml
524
+ # ~/.automatic/config/web2markdown.yml
525
+ plugins:
526
+ - module: CustomFeedWeb
527
+ config:
528
+ retry: 2
529
+ interval: 2
530
+ sites:
531
+ - name: The Go Blog
532
+ url: https://go.dev/blog/
533
+ link_selector: 'a[href]'
534
+ include:
535
+ - '^https://go\.dev/blog/[^/]+$'
536
+ same_host: true
537
+ fetch_items: 20
538
+
539
+ - module: StoreDigest
540
+ config:
541
+ db: web2markdown.db
542
+ fields:
543
+ - title
544
+ - link
545
+
546
+ - module: PublishMarkdown
547
+ config:
548
+ file: ~/.automatic/markdown/web.md
549
+ mode: append
550
+ ```
551
+
552
+ [`QUICKSTART.md`](QUICKSTART.md) runs that Recipe end to end, with the sites it
553
+ watches and what each plugin does. What follows is the part a checkout does
554
+ differently: turning the Recipe's plugins into a bundle that can run them.
555
+
556
+ **1. List the plugins the Recipe names.** They are the `module` lines, in order:
557
+ `CustomFeedWeb`, `StoreDigest`, `PublishMarkdown`.
558
+
559
+ **2. Look each one up in the table above.** What that lookup yields here, and
560
+ the whole of what this Recipe's dependencies are:
561
+
562
+ | Plugin | Needs | Checkout group |
563
+ | --- | --- | --- |
564
+ | `CustomFeedWeb` | `nokogiri`, to read the index pages | `html` |
565
+ | `StoreDigest` | `activerecord`, `sqlite3` | `store` |
566
+ | `PublishMarkdown` | nothing of its own | — |
567
+
568
+ `PublishMarkdown` is the row worth reading twice. It needs no gem: it reduces an
569
+ HTML body with `nokogiri` where one is installed and with its own substitution
570
+ where none is, so it neither adds a group here nor fails without one. A plugin's
571
+ row in the table is the answer, not a guess from what the plugin does.
572
+
573
+ **3. Add the groups up.** This Recipe needs `html` **and** `store`. Getting this
574
+ step half right fails half way through the run, because a plugin is loaded when
575
+ the pipeline reaches it and not before: with `html` alone, `CustomFeedWeb`
576
+ loads, fetches its pages and hands its feeds on, and the run then stops where
577
+ the second plugin is loaded.
578
+
579
+ ```text
580
+ automatic: The `activerecord` gem is not installed. It is needed by the store
581
+ plugins. Install it with `gem install activerecord`, or in a source checkout add
582
+ its group to the bundle; see the optional plugin dependencies in
583
+ doc/DEPLOYMENT.md. (cannot load such file -- active_record)
584
+ ```
585
+
586
+ The first plugin having worked is not the setup being finished. Read the whole
587
+ Recipe, then install once.
588
+
589
+ **4. Select the groups in the checkout.**
590
+
591
+ ```sh
592
+ cd ~/automaticruby
593
+ bundle config set --local with "html store"
594
+ ```
595
+
596
+ **5. Install them.**
597
+
598
+ ```sh
599
+ bundle install
600
+ ```
601
+
602
+ **6. Check what was selected.** This prints the setting Bundler will use, and
603
+ where it came from:
604
+
605
+ ```sh
606
+ bundle config get with
607
+ ```
608
+
609
+ ```text
610
+ Set for your local app (/home/you/automaticruby/.bundle/config): [:html, :store]
611
+ ```
612
+
613
+ **7. Run the Recipe.**
614
+
615
+ ```sh
616
+ bundle exec bin/automatic -c ~/.automatic/config/web2markdown.yml
617
+ ```
618
+
619
+ Run it twice. The second run publishes nothing, because `StoreDigest` has the
620
+ digest of everything the page listed, which is what makes the Recipe safe to put
621
+ in `cron` and is worth checking before scheduling any Recipe with an effect.
622
+
623
+ A store plugin's `db` is a **file name**, not a path: it is resolved under
624
+ `~/.automatic/db`, or under the checkout's own `db/` where that directory does
625
+ not exist yet, so a checkout run before `scaffold` keeps its database inside the
626
+ checkout. The `Using Database:` line of an `info`-level log names the file that
627
+ was opened, which is the way to check. `PublishMarkdown`'s `file` is the
628
+ opposite — a path, with `~` expanded — and the Recipe above uses each as it is
629
+ meant.
630
+
631
+ Adding a plugin to a Recipe later starts this over at step 1: a `FilterSanitize`
632
+ added to the Recipe above brings `sanitize` with it, and the `with` setting has
633
+ to name `sanitize` as well as `html` and `store`.
634
+
635
+ ### Why `bundle exec`, and why `gem install` is not enough here
636
+
637
+ A checkout resolves its gems through Bundler whether or not you ask it to.
638
+ `lib/automatic/environment.rb` treats a `Gemfile` beside `lib/` as "this is a
639
+ source checkout" and requires `bundler/setup` before anything else loads, so
640
+ `bin/automatic` run from a checkout sees the bundle and nothing outside it. That
641
+ is what makes `gem install` the wrong tool there: the gem installs, `gem list`
642
+ prints it, plain `ruby -rnokogiri -e ''` loads it — and the checkout still
643
+ reports it as missing, because it is not in the bundle.
644
+
645
+ ```sh
646
+ gem install nokogiri
647
+ gem list nokogiri # prints the gem that was just installed
648
+ ./bin/automatic -c ~/.automatic/config/web2markdown.yml
649
+ # automatic: The `nokogiri` gem is not installed. It is needed by CustomFeedWeb...
650
+ ```
651
+
652
+ Use the group, and then run through `bundle exec`, from the checkout directory:
653
+
654
+ ```sh
655
+ cd ~/automaticruby
656
+ bundle exec bin/automatic -c ~/.automatic/config/web2markdown.yml
657
+ ```
658
+
659
+ `bundle exec` matters for what it does when the bundle is **not** in the state
660
+ you think it is. The `require 'bundler/setup'` above is deliberately forgiving:
661
+ where Bundler is absent, or where a group has been selected but not yet
662
+ installed, it is rescued and the program falls back to whatever RubyGems can
663
+ activate — quietly, and against gems the `Gemfile.lock` never resolved.
664
+ Running the same command through `bundle exec` says so instead:
665
+
666
+ ```text
667
+ bundler: failed to load command: bin/automatic (bin/automatic)
668
+ Could not find activerecord-8.1.3.1, sqlite3-2.9.6-x86_64-linux-gnu ... in
669
+ locally installed gems (Bundler::GemNotFound)
670
+ ```
671
+
672
+ That is `bundle install` not having been run after step 4, stated as such.
673
+ `bundle exec` is also what keeps one habit for the whole checkout: the same
674
+ prefix runs the specs, the diagnostic subcommands and the Recipe, and typing
675
+ `automatic` instead would run the installed gem rather than the checkout.
676
+
677
+ **What "is not installed" means in that message.** It is the framework
678
+ reporting that **this process could not `require` the library**, which is wider
679
+ than "no copy of the gem exists on this machine". A gem installed by `gem
680
+ install` but outside the checkout's bundle produces it; so does a group selected
681
+ but not installed, and so does an installation under a different Ruby. The gem
682
+ name and the plugin name in the message are what to act on; whether to act with
683
+ a group or with `gem install` is decided by where you are running from, not by
684
+ the message.
685
+
686
+ To see what the bundle actually holds, ask the bundle rather than RubyGems:
687
+
688
+ ```sh
689
+ bundle config get with # which optional groups are selected
690
+ bundle show nokogiri # where the bundle's copy is, or an error
691
+ bundle show activerecord
692
+ bundle show sqlite3
693
+ ```
694
+
695
+ ```sh
696
+ bundle exec ruby -rnokogiri -e 'puts Nokogiri::VERSION'
697
+ bundle exec ruby -ractive_record -e 'puts ActiveRecord::VERSION::STRING'
698
+ bundle exec ruby -rsqlite3 -e 'puts SQLite3::VERSION'
699
+ ```
700
+
701
+ Those commands `require` the libraries the way the plugins do — note
702
+ `active_record` for the `activerecord` gem — under the same bundle the Recipe
703
+ will run under. `gem list` answers a different question and is the one to
704
+ distrust here: it lists what RubyGems has, which in a checkout is neither what
705
+ the plugins will load nor what a missing-gem message is about.
706
+
707
+ ### All of the optional gems, or only the ones a Recipe names
708
+
709
+ Choose the aggregate `plugins` group for plugin development, or
710
+ purpose-specific groups for the Recipe being run:
711
+
712
+ ```sh
713
+ bundle config set --local with plugins # all of them, at once
714
+ bundle config set --local with "html store" # what this Recipe needs
715
+ ```
716
+
717
+ `plugins` is every gem in the first block of the `Gemfile` — `activerecord`,
718
+ `sqlite3`, `nokogiri`, `sanitize`, `feedbag` — and is meant for working **on**
719
+ the plugins: it brings their specs into the ordinary `bundle exec rake` run,
720
+ which is how those plugins are verified. It is the right setting for plugin
721
+ development and for a checkout you are developing in.
722
+
723
+ Naming the groups is the right setting for **running** Recipes, and is what the
724
+ design of the split asks for: a gem is installed by the operator who uses the
725
+ plugin that needs it, and a checkout that runs this Recipe has no reason to
726
+ build `sanitize` or to hold `feedbag`. Start there; `plugins` is not a shortcut
727
+ for having read the table, and a checkout that has installed everything hides
728
+ the group a Recipe of yours will need on the next machine.
729
+
730
+ Either way the setting is written to the checkout's own `.bundle/config`, which
731
+ is not committed and belongs to that checkout alone. That is what makes it hold:
732
+ `bundle install`, `bundle exec bin/automatic`, `bundle exec rake` and the
733
+ checkout's `bin/automatic` all read it, so the groups are selected once rather
734
+ than remembered at every command. Passing the environment variable instead —
735
+ `BUNDLE_WITH="html store" bundle install` — configures that one command and
736
+ leaves the next one without it, which produces exactly the confusing case above:
737
+ an installed group that the running process does not select.
738
+
739
+ To return the checkout to the minimum:
740
+
741
+ ```sh
742
+ bundle config unset --local with
743
+ bundle install
744
+ ```
745
+
746
+ The gems stay on the machine; what changes is that the bundle no longer includes
747
+ them, and the plugins that need them report them as missing again.
748
+
749
+ ## Your own plugins
750
+
751
+ `~/.automatic/plugins` is on the loader's search path, **ahead of the
752
+ installation**, so a plugin you put there is found first and a file named like a
753
+ shipped plugin replaces it. Overriding a shipped plugin without editing the
754
+ installation is the intended use.
755
+
756
+ ```sh
757
+ $EDITOR ~/.automatic/plugins/filter/my_filter.rb
758
+ ```
759
+
760
+ The class is `Automatic::Plugin::FilterMyFilter`; the contract and a worked
761
+ example are in [`PLUGINS.md`](PLUGINS.md) sections 3 and 4. A gem upgrade does
762
+ not touch this directory.
763
+
764
+ ## When something fails
765
+
766
+ **`command not found: automatic`** — the gem's binary directory is not on
767
+ `PATH`. `gem environment` prints it as EXECUTABLE DIRECTORY.
768
+
769
+ **`The <gem> gem is not installed. It is needed by ...`** — a plugin's optional
770
+ dependency could not be required. The message names the gem, the plugin and the
771
+ command to install it, and the table above says the same thing. In a checkout it
772
+ also means a gem installed outside the bundle, or a group selected but not
773
+ installed; "Why `bundle exec`, and why `gem install` is not enough here" above
774
+ tells the cases apart.
775
+
776
+ **`Automatic::NoPluginError: unknown plugin named ...`** — a Recipe names a
777
+ plugin that does not ship. Check the spelling against
778
+ [`PLUGINS.md`](PLUGINS.md) section 6; if the name is in section 8, the plugin
779
+ was removed because the service behind it no longer exists, and the Recipe
780
+ needs a different last step rather than a reinstall. Nothing has run when this
781
+ is raised.
782
+
783
+ **`unknown plugin named X`** — the Recipe names a module the loader cannot
784
+ resolve. Check the spelling against [`PLUGINS.md`](PLUGINS.md) section 6, and
785
+ check the class-name-to-path rule in section 3.2 if it is your own plugin — a
786
+ file in the wrong category directory is never found.
787
+
788
+ **A Recipe path is not found** — a bare name is looked for in
789
+ `~/.automatic/config` only. Use a path with a `/` in it for anything else.
790
+
791
+ **The run stops partway** — a plugin raised, and the framework does not catch
792
+ plugin exceptions. Everything before it has already happened; there is no
793
+ rollback. Rerun after fixing the cause. If the plugins after the failure must
794
+ not repeat their effect, that is what a store plugin in front of them is for.
795
+
796
+ **A run repeats work it did last time** — the Recipe has no store plugin, or the
797
+ database it names is not where you think. Check the `Using Database:` line in
798
+ an `info`-level log.
799
+
800
+ **Nothing is published, but nothing failed either** — a filter dropped
801
+ everything, or the store plugin had already seen it all. Insert
802
+ `PublishConsoleLink` between steps to see what the pipeline holds where.
803
+
804
+ **A plugin behaves as though its settings are absent** — settings are read by
805
+ string key, and a typo in a key is silently `nil`. Check the key names against
806
+ the plugin's table in `PLUGINS.md` section 6.
807
+
808
+ ## Running the integration recipes
809
+
810
+ `test/integration/` holds Recipes used to exercise plugins against real
811
+ services. They are run by hand, they are not part of the test suite, and they
812
+ are not run in CI:
813
+
814
+ ```sh
815
+ bundle exec bin/automatic -c test/integration/test_sort.yml
816
+ ```
817
+
818
+ Most of them need a credential or a service you run. Read
819
+ the Recipe before running it, and check the plugin's status in
820
+ [`PLUGINS.md`](PLUGINS.md) section 6.
821
+
822
+ ## Uninstalling
823
+
824
+ ```sh
825
+ gem uninstall automatic
826
+ rm -rf ~/.automatic # or: automatic unscaffold, before uninstalling
827
+ ```
828
+
829
+ Files that Recipes wrote outside `~/.automatic` — downloads under a `path` you
830
+ configured, for instance — are yours to remove.