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