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.
- checksums.yaml +5 -5
- data/README.md +635 -83
- data/VERSION +1 -1
- data/automatic.gemspec +109 -248
- data/bin/automatic +20 -139
- data/config/feed2console.yml +10 -4
- data/config/feed2markdown.yml +41 -0
- data/doc/AI_TUTORIAL.md +518 -0
- data/doc/BASIC_DESIGN.md +516 -0
- data/doc/COPYING.LESSER +165 -0
- data/doc/DEPLOYMENT.md +824 -0
- data/doc/LICENSE.md +14 -0
- data/doc/PLUGINS.md +1875 -0
- data/doc/PLUGIN_DEVELOPMENT.md +86 -0
- data/doc/POLICY.md +857 -0
- data/doc/QUICKSTART.md +256 -0
- data/doc/RELEASING.md +381 -0
- data/doc/REQUIREMENTS.md +526 -0
- data/doc/VERSIONS +208 -0
- data/lib/automatic/cli.rb +248 -0
- data/lib/automatic/environment.rb +31 -5
- data/lib/automatic/feed_maker.rb +10 -9
- data/lib/automatic/feed_parser.rb +51 -35
- data/lib/automatic/http.rb +107 -0
- data/lib/automatic/log.rb +49 -18
- data/lib/automatic/opml.rb +3 -1
- data/lib/automatic/pipeline.rb +63 -32
- data/lib/automatic/recipe.rb +56 -17
- data/lib/automatic/version.rb +14 -1
- data/lib/automatic.rb +78 -20
- data/plugins/custom_feed/svn_log.rb +73 -32
- data/plugins/custom_feed/web.rb +348 -0
- data/plugins/filter/absolute_uri.rb +43 -27
- data/plugins/filter/accept.rb +38 -45
- data/plugins/filter/claude.rb +217 -0
- data/plugins/filter/clear.rb +12 -8
- data/plugins/filter/description_link.rb +49 -51
- data/plugins/filter/full_feed.rb +158 -52
- data/plugins/filter/gemini.rb +216 -0
- data/plugins/filter/github_feed.rb +38 -26
- data/plugins/filter/ignore.rb +33 -44
- data/plugins/filter/image.rb +36 -25
- data/plugins/filter/image_source.rb +58 -52
- data/plugins/filter/join.rb +107 -0
- data/plugins/filter/one.rb +19 -26
- data/plugins/filter/open_ai.rb +198 -0
- data/plugins/filter/rand.rb +16 -17
- data/plugins/filter/sakura_ai.rb +205 -0
- data/plugins/filter/sanitize.rb +29 -34
- data/plugins/filter/sort.rb +20 -27
- data/plugins/filter/tumblr_resize.rb +31 -23
- data/plugins/notify/ikachan.rb +86 -48
- data/plugins/provide/fluentd.rb +43 -24
- data/plugins/publish/amazon_s3.rb +73 -40
- data/plugins/publish/console.rb +19 -16
- data/plugins/publish/console_link.rb +20 -17
- data/plugins/publish/eject.rb +48 -26
- data/plugins/publish/fluentd.rb +50 -30
- data/plugins/publish/hatena_bookmark.rb +88 -71
- data/plugins/publish/instapaper.rb +69 -59
- data/plugins/publish/markdown.rb +278 -0
- data/plugins/publish/memcached.rb +35 -29
- data/plugins/store/database.rb +50 -48
- data/plugins/store/digest.rb +212 -0
- data/plugins/store/file.rb +99 -68
- data/plugins/store/full_text.rb +32 -25
- data/plugins/store/permalink.rb +18 -22
- data/plugins/subscription/feed.rb +34 -21
- data/plugins/subscription/link.rb +31 -32
- data/plugins/subscription/text.rb +32 -46
- data/plugins/subscription/tumblr.rb +55 -44
- data/plugins/subscription/xml.rb +40 -36
- metadata +108 -321
- data/Gemfile +0 -38
- data/Rakefile +0 -59
- data/doc/ChangeLog +0 -303
- data/doc/PLUGINS +0 -750
- data/doc/PLUGINS.ja +0 -753
- data/doc/README +0 -511
- data/doc/README.ja +0 -519
- data/plugins/filter/google_news.rb +0 -50
- data/plugins/publish/google_calendar.rb +0 -84
- data/plugins/publish/hipchat.rb +0 -46
- data/plugins/publish/pocket.rb +0 -45
- data/plugins/publish/twitter.rb +0 -58
- data/plugins/subscription/chan_toru.rb +0 -57
- data/plugins/subscription/g_guide.rb +0 -57
- data/plugins/subscription/pocket.rb +0 -51
- data/plugins/subscription/twitter.rb +0 -69
- data/plugins/subscription/twitter_search.rb +0 -50
- data/plugins/subscription/weather.rb +0 -33
- data/script/build +0 -84
- data/spec/fixtures/sampleFeeds.tsv +0 -1
- data/spec/fixtures/sampleFeeds2.tsv +0 -2
- data/spec/fixtures/sampleRecipe.yml +0 -24
- data/spec/lib/automatic/log_spec.rb +0 -32
- data/spec/lib/automatic/pipeline_spec.rb +0 -68
- data/spec/lib/automatic/recipe_spec.rb +0 -40
- data/spec/lib/automatic_spec.rb +0 -99
- data/spec/plugins/custom_feed/svn_log_spec.rb +0 -31
- data/spec/plugins/filter/absolute_uri_spec.rb +0 -61
- data/spec/plugins/filter/accept_spec.rb +0 -331
- data/spec/plugins/filter/clear_spec.rb +0 -49
- data/spec/plugins/filter/description_link_spec.rb +0 -138
- data/spec/plugins/filter/full_feed_spec.rb +0 -129
- data/spec/plugins/filter/github_feed_spec.rb +0 -55
- data/spec/plugins/filter/google_news_spec.rb +0 -69
- data/spec/plugins/filter/ignore_spec.rb +0 -328
- data/spec/plugins/filter/image_source_spec.rb +0 -89
- data/spec/plugins/filter/image_spec.rb +0 -65
- data/spec/plugins/filter/one_spec.rb +0 -71
- data/spec/plugins/filter/rand_spec.rb +0 -52
- data/spec/plugins/filter/sanitize_spec.rb +0 -153
- data/spec/plugins/filter/sort_spec.rb +0 -189
- data/spec/plugins/filter/tumblr_resize_spec.rb +0 -109
- data/spec/plugins/notify/ikachan_spec.rb +0 -58
- data/spec/plugins/provide/fluentd_spec.rb +0 -49
- data/spec/plugins/publish/amazon_s3_spec.rb +0 -40
- data/spec/plugins/publish/console_spec.rb +0 -30
- data/spec/plugins/publish/eject_spec.rb +0 -40
- data/spec/plugins/publish/fluentd_spec.rb +0 -40
- data/spec/plugins/publish/google_calendar_spec.rb +0 -83
- data/spec/plugins/publish/hatena_bookmark_spec.rb +0 -134
- data/spec/plugins/publish/hipchat_spec.rb +0 -69
- data/spec/plugins/publish/instapaper_spec.rb +0 -82
- data/spec/plugins/publish/memcached_spec.rb +0 -63
- data/spec/plugins/publish/pocket_spec.rb +0 -51
- data/spec/plugins/publish/twitter_spec.rb +0 -73
- data/spec/plugins/store/file_spec.rb +0 -58
- data/spec/plugins/store/full_text_spec.rb +0 -152
- data/spec/plugins/store/permalink_spec.rb +0 -206
- data/spec/plugins/subscription/chan_toru_spec.rb +0 -56
- data/spec/plugins/subscription/feed_spec.rb +0 -71
- data/spec/plugins/subscription/g_guide_spec.rb +0 -82
- data/spec/plugins/subscription/link_spec.rb +0 -72
- data/spec/plugins/subscription/pocket_spec.rb +0 -57
- data/spec/plugins/subscription/text_spec.rb +0 -84
- data/spec/plugins/subscription/tumblr_spec.rb +0 -74
- data/spec/plugins/subscription/twitter_search_spec.rb +0 -57
- data/spec/plugins/subscription/twitter_spec.rb +0 -73
- data/spec/plugins/subscription/weather_spec.rb +0 -44
- data/spec/plugins/subscription/xml_spec.rb +0 -84
- data/spec/spec_helper.rb +0 -106
- data/spec/user_dir/plugins/store/mock.rb +0 -16
- data/test/fixtures/sampleOPML.xml +0 -11
- data/test/integration/test_absoluteurl.yml +0 -25
- data/test/integration/test_activerecord.yml +0 -24
- data/test/integration/test_add_pocket.yml +0 -26
- data/test/integration/test_chan_toru.yml +0 -21
- data/test/integration/test_descriptionlink.yml +0 -21
- data/test/integration/test_fluentd.yml +0 -22
- data/test/integration/test_fulltext.yml +0 -30
- data/test/integration/test_google_news.yml +0 -21
- data/test/integration/test_googlealert.yml +0 -21
- data/test/integration/test_hatenabookmark.yml +0 -30
- data/test/integration/test_ignore.yml +0 -25
- data/test/integration/test_ignore2.yml +0 -22
- data/test/integration/test_image2local.yml +0 -33
- data/test/integration/test_instapaper.yml +0 -26
- data/test/integration/test_link2local.yml +0 -34
- data/test/integration/test_one.yml +0 -23
- data/test/integration/test_pocket.yml +0 -22
- data/test/integration/test_rand.yml +0 -21
- data/test/integration/test_sanitize.yml +0 -23
- data/test/integration/test_sort.yml +0 -36
- data/test/integration/test_svnlog.yml +0 -15
- data/test/integration/test_text2feed.yml +0 -36
- data/test/integration/test_tumblr2local.yml +0 -43
- data/test/integration/test_twitter_search.yml +0 -22
- data/test/integration/test_weather.yml +0 -19
- data/test/integration/test_xml2fluentd.yml +0 -21
- 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.
|