automatic 14.12.2 → 26.09
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +5 -5
- data/README.md +669 -83
- data/VERSION +1 -1
- data/assets/siteinfo/items_all.json +60300 -52138
- data/automatic.gemspec +110 -248
- data/bin/automatic +20 -139
- data/config/feed2console.yml +10 -4
- data/config/feed2markdown.yml +41 -0
- data/doc/AI_TUTORIAL.md +542 -0
- data/doc/BASIC_DESIGN.md +547 -0
- data/doc/COPYING.LESSER +165 -0
- data/doc/DEPLOYMENT.md +830 -0
- data/doc/LICENSE.md +14 -0
- data/doc/PLUGINS.md +2015 -0
- data/doc/PLUGIN_DEVELOPMENT.md +86 -0
- data/doc/POLICY.md +962 -0
- data/doc/QUICKSTART.md +260 -0
- data/doc/RELEASING.md +394 -0
- data/doc/REQUIREMENTS.md +556 -0
- data/doc/VERSIONS +266 -0
- data/lib/automatic/cli.rb +270 -0
- data/lib/automatic/environment.rb +31 -5
- data/lib/automatic/feed_maker.rb +65 -25
- 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 +73 -32
- data/lib/automatic/recipe.rb +100 -17
- data/lib/automatic/version.rb +14 -1
- data/lib/automatic.rb +93 -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/batch.rb +97 -0
- data/plugins/filter/claude.rb +217 -0
- data/plugins/filter/clear.rb +12 -8
- data/plugins/filter/description_link.rb +66 -51
- data/plugins/filter/full_feed.rb +170 -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 +75 -52
- data/plugins/filter/join.rb +105 -0
- data/plugins/filter/kimi.rb +216 -0
- data/plugins/filter/limit.rb +57 -0
- data/plugins/filter/one.rb +19 -26
- data/plugins/filter/open_ai.rb +198 -0
- data/plugins/filter/present.rb +75 -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 +79 -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 +107 -68
- data/plugins/store/full_text.rb +31 -28
- 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 +40 -45
- data/plugins/subscription/tumblr.rb +55 -44
- data/plugins/subscription/xml.rb +40 -36
- metadata +112 -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/README.md
CHANGED
|
@@ -1,146 +1,732 @@
|
|
|
1
|
-
Automatic Ruby
|
|
2
|
-
==============
|
|
1
|
+
# Automatic Ruby
|
|
3
2
|
|
|
4
|
-
**Ruby framework for
|
|
3
|
+
**A Ruby framework for general-purpose automatic processing.**
|
|
5
4
|
|
|
5
|
+
Automatic Ruby runs jobs that you *assemble* rather than write. A step is a
|
|
6
|
+
plugin — a small Ruby class with one method. A job is a **Recipe** — a YAML file
|
|
7
|
+
naming the plugins in order, with their settings. Running the job is one
|
|
8
|
+
command, which is safe to put in `cron`.
|
|
6
9
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
```yaml
|
|
11
|
+
plugins:
|
|
12
|
+
- module: SubscriptionFeed
|
|
13
|
+
config:
|
|
14
|
+
feeds:
|
|
15
|
+
- https://www.ruby-lang.org/en/feeds/news.rss
|
|
13
16
|
|
|
14
|
-
|
|
17
|
+
- module: StorePermalink
|
|
18
|
+
config:
|
|
19
|
+
db: seen.db
|
|
15
20
|
|
|
21
|
+
- module: PublishMarkdown
|
|
22
|
+
config:
|
|
23
|
+
file: ~/notes/feeds.md
|
|
24
|
+
```
|
|
16
25
|
|
|
17
|
-
|
|
18
|
-
|
|
26
|
+
```sh
|
|
27
|
+
automatic -c my_recipe.yml
|
|
28
|
+
```
|
|
19
29
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
30
|
+
Fetch a public feed, remember what has already been seen, and write the new
|
|
31
|
+
items into a Markdown file: readable by people, reusable by
|
|
32
|
+
tools, ready to hand to an AI. Change the last plugin and the same pipeline
|
|
33
|
+
prints to the terminal instead, or downloads the images, or forwards them to
|
|
34
|
+
Fluentd, or writes them to a database. Write your own plugin and it composes
|
|
35
|
+
with all the others.
|
|
36
|
+
|
|
37
|
+
**[Follow the Quick Start](doc/QUICKSTART.md)** to install the gem, write a
|
|
38
|
+
Recipe that watches public index pages, install what that Recipe needs, and
|
|
39
|
+
produce Markdown from the articles they list.
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## Contents
|
|
44
|
+
|
|
45
|
+
1. [Overview](#1-overview)
|
|
46
|
+
2. [Features](#2-features)
|
|
47
|
+
3. [Architecture](#3-architecture)
|
|
48
|
+
4. [Supported environment](#4-supported-environment)
|
|
49
|
+
5. [Installation](#5-installation)
|
|
50
|
+
6. [Quick start](#6-quick-start)
|
|
51
|
+
7. [Recipes](#7-recipes)
|
|
52
|
+
8. [Plugins](#8-plugins)
|
|
53
|
+
9. [CLI usage](#9-cli-usage)
|
|
54
|
+
10. [Configuration](#10-configuration)
|
|
55
|
+
11. [The user directory](#11-the-user-directory)
|
|
56
|
+
12. [Testing](#12-testing)
|
|
57
|
+
13. [Development](#13-development)
|
|
58
|
+
14. [Repository structure](#14-repository-structure)
|
|
59
|
+
15. [Documents](#15-documents)
|
|
60
|
+
16. [Versioning](#16-versioning)
|
|
61
|
+
17. [Name](#17-name)
|
|
62
|
+
18. [Primary Branch](#18-primary-branch)
|
|
63
|
+
19. [License](#19-license)
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## 1. Overview
|
|
68
|
+
|
|
69
|
+
A recurring job usually has the same shape: fetch something, decide which parts
|
|
70
|
+
of it are new or interesting, keep a record so the same item is not handled
|
|
71
|
+
twice, and send the result somewhere. Writing each such job as a script means
|
|
72
|
+
writing the fetching, the filtering, the de-duplication and the retrying again
|
|
73
|
+
every time.
|
|
74
|
+
|
|
75
|
+
"Somewhere" is often a file. A Recipe that ends in `PublishMarkdown` leaves what
|
|
76
|
+
it collected as a Markdown document: a person reads it as it is, `grep` searches
|
|
77
|
+
it, Git keeps its history, and a program — a language model or an agent among
|
|
78
|
+
them — takes it as input without a parser or an API. That is the general case,
|
|
79
|
+
and it needs no account anywhere; the plugins that publish to a service are for
|
|
80
|
+
when a particular service is the point.
|
|
81
|
+
|
|
82
|
+
Automatic Ruby is neither host infrastructure nor a purpose-built
|
|
83
|
+
application. It is a general-purpose composition framework: a small mechanism
|
|
84
|
+
for assembling independent steps into a straight-line job. Its generality
|
|
85
|
+
comes from how many jobs fit that composition model, not from growing the
|
|
86
|
+
framework until every kind of workflow fits inside it.
|
|
87
|
+
|
|
88
|
+
When one concrete purpose needs branching, transactional orchestration,
|
|
89
|
+
multi-user state, permissions, a strongly coupled domain model, or another
|
|
90
|
+
responsibility that does not fit a short plugin pipeline naturally, a
|
|
91
|
+
purpose-built application is usually the better design. Automatic Ruby is
|
|
92
|
+
not made more general by absorbing that complexity into its core.
|
|
93
|
+
|
|
94
|
+
Automatic Ruby exists so that those jobs are assembled instead. It contributes
|
|
95
|
+
exactly three things:
|
|
96
|
+
|
|
97
|
+
- **a uniform contract** every step obeys, so that steps compose,
|
|
98
|
+
- **one value** passed from step to step, so that they have something to compose
|
|
99
|
+
over,
|
|
100
|
+
- **a loader** that finds a step by name, so that a Recipe can name it.
|
|
101
|
+
|
|
102
|
+
Everything else is a plugin. The framework is intentionally kept small; its
|
|
103
|
+
responsibilities and architectural boundary are defined in
|
|
104
|
+
[`doc/BASIC_DESIGN.md`](doc/BASIC_DESIGN.md) and
|
|
105
|
+
[`doc/POLICY.md`](doc/POLICY.md).
|
|
106
|
+
|
|
107
|
+
It is one person's tooling, run unattended from `cron`, against their own
|
|
108
|
+
accounts and their own files. It is not a service, and there is no notion of a
|
|
109
|
+
second user.
|
|
110
|
+
|
|
111
|
+
The project began in February 2012 and this is the first release since 2015. The
|
|
112
|
+
core, the Recipe format and the plugin contract are unchanged; what has changed
|
|
113
|
+
is that it runs on a current Ruby, installs from a current RubyGems, and ships
|
|
114
|
+
a plugin set every part of which still has somewhere to talk to. See
|
|
115
|
+
[`doc/VERSIONS`](doc/VERSIONS).
|
|
116
|
+
|
|
117
|
+
## 2. Features
|
|
118
|
+
|
|
119
|
+
- **Recipes in YAML.** A job is a file, not a program. No Ruby is written to
|
|
120
|
+
wire a pipeline together.
|
|
121
|
+
- **Plugins organized by role.** Subscribe, custom feed, filter, store, provide,
|
|
122
|
+
notify and publish plugins compose through the same pipeline contract.
|
|
123
|
+
- **Markdown out of the box.** `PublishMarkdown` writes the result as a plain
|
|
124
|
+
Markdown document, to a file or to standard output, with no service and no
|
|
125
|
+
credential behind it. It is the natural end of a new Recipe.
|
|
126
|
+
- **Plugins are found, not registered.** Adding one is dropping a file in a
|
|
127
|
+
directory. No framework file is edited.
|
|
128
|
+
- **Your plugins override the shipped ones.** `~/.automatic/plugins` is searched
|
|
129
|
+
first, so a shipped plugin can be replaced without touching the installation.
|
|
130
|
+
- **De-duplication built in.** The store plugins keep a SQLite record of what
|
|
131
|
+
has been seen, which is what makes a Recipe safe to run every hour. Their
|
|
132
|
+
gems are installed when you use them, not before.
|
|
133
|
+
- **Retry and interval** on everything that reaches the network, configured per
|
|
134
|
+
plugin in the Recipe.
|
|
135
|
+
- **A small installation.** A gem needed by one plugin is not a dependency of
|
|
136
|
+
the framework: `gem install automatic` brings the framework's runtime
|
|
137
|
+
dependencies declared in `automatic.gemspec` and the command, and installs
|
|
138
|
+
neither an HTML parser nor a database — let alone an AWS SDK.
|
|
139
|
+
- **No museum.** Every plugin is classified, with its reason, in
|
|
140
|
+
[`doc/PLUGINS.md`](doc/PLUGINS.md). Nothing dead is stubbed into looking
|
|
141
|
+
alive, and an integration whose service has gone is removed rather than
|
|
142
|
+
kept as a fossil.
|
|
143
|
+
|
|
144
|
+
## 3. Architecture
|
|
145
|
+
|
|
146
|
+
```text
|
|
147
|
+
bin/automatic process entry point; exit status only
|
|
148
|
+
|
|
|
149
|
+
v
|
|
150
|
+
Automatic::CLI options, subcommands, error reporting
|
|
151
|
+
|
|
|
152
|
+
v
|
|
153
|
+
Automatic -> Recipe -> Pipeline -> Automatic::Plugin::*
|
|
154
|
+
| |
|
|
155
|
+
v v
|
|
156
|
+
Automatic::Log Automatic::FeedMaker / FeedParser
|
|
27
157
|
```
|
|
28
158
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
159
|
+
One run is: load the Recipe, then for each plugin entry in order, load the
|
|
160
|
+
class, construct it with its settings and the current pipeline, call `run`, and
|
|
161
|
+
take the result as the input to the next.
|
|
162
|
+
|
|
163
|
+
```ruby
|
|
164
|
+
pipeline = []
|
|
165
|
+
recipe.each_plugin do |plugin|
|
|
166
|
+
mod = plugin.module
|
|
167
|
+
load_plugin(mod)
|
|
168
|
+
klass = Automatic::Plugin.const_get(mod)
|
|
169
|
+
pipeline = klass.new(plugin.config, pipeline).run
|
|
170
|
+
end
|
|
37
171
|
```
|
|
38
172
|
|
|
39
|
-
|
|
173
|
+
That is the whole of the framework's behaviour. The value passed along — the
|
|
174
|
+
*pipeline* — is an array of feed objects, and because every plugin takes and
|
|
175
|
+
returns that one shape, any plugin composes with any other.
|
|
40
176
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
177
|
+
This composability is an architectural invariant, not an optimization
|
|
178
|
+
preference. Keeping the core small, keeping plugin responsibilities
|
|
179
|
+
independent and keeping one pipeline shape are what make Automatic Ruby this
|
|
180
|
+
framework rather than a collection of hard-coded applications.
|
|
181
|
+
|
|
182
|
+
The pipeline normally **narrows**: subscription plugins produce, filters and
|
|
183
|
+
stores reduce, publishers consume. Reading a Recipe top to bottom reads the
|
|
184
|
+
data flow.
|
|
185
|
+
|
|
186
|
+
The full account is [`doc/BASIC_DESIGN.md`](doc/BASIC_DESIGN.md).
|
|
187
|
+
|
|
188
|
+
## 4. Supported environment
|
|
44
189
|
|
|
45
|
-
|
|
190
|
+
- **Ruby 3.3 through 4.0.** The continuously validated versions are the matrix
|
|
191
|
+
in [`.github/workflows/ci.yml`](.github/workflows/ci.yml).
|
|
192
|
+
- A Unix-like system. GNU/Linux and macOS are what it is used on. Windows is not
|
|
193
|
+
supported.
|
|
194
|
+
- A compiler only if you install an optional plugin gem that builds from source
|
|
195
|
+
on your platform, such as `nokogiri` or `sqlite3`. The framework's own
|
|
196
|
+
dependencies are pure Ruby.
|
|
46
197
|
|
|
47
|
-
|
|
48
|
-
|
|
198
|
+
Ruby 3.3 is the floor: it is the oldest maintained release the dependencies are
|
|
199
|
+
resolved and tested against. Nothing older is tested or supported.
|
|
200
|
+
|
|
201
|
+
Two statements, and they are not the same one:
|
|
202
|
+
|
|
203
|
+
- **Supported range.** The code is written for Ruby 3.3 through 4.0, using APIs
|
|
204
|
+
the whole range shares. `required_ruby_version` is `>= 3.3.0` and has no upper
|
|
205
|
+
bound, so a Ruby newer than the matrix is permitted rather than refused.
|
|
206
|
+
- **Continuously validated versions.** The authoritative set is the matrix in
|
|
207
|
+
[`.github/workflows/ci.yml`](.github/workflows/ci.yml), which represents the
|
|
208
|
+
supported range rather than enumerating every intermediate release. A
|
|
209
|
+
version's absence from the matrix means it is not verified on every commit;
|
|
210
|
+
it does not mean it is expected to fail.
|
|
211
|
+
|
|
212
|
+
## 5. Installation
|
|
213
|
+
|
|
214
|
+
### From RubyGems
|
|
215
|
+
|
|
216
|
+
The released gem is available as [automatic on RubyGems](https://rubygems.org/gems/automatic).
|
|
217
|
+
|
|
218
|
+
```sh
|
|
219
|
+
gem install automatic
|
|
220
|
+
automatic --version
|
|
49
221
|
```
|
|
50
222
|
|
|
223
|
+
That installs the framework, the command and the runtime dependencies
|
|
224
|
+
declared in `automatic.gemspec`.
|
|
225
|
+
A gem that only one plugin needs is not among them: install it when you use
|
|
226
|
+
that plugin, with `gem install nokogiri` or `gem install activerecord sqlite3`.
|
|
227
|
+
[`doc/DEPLOYMENT.md`](doc/DEPLOYMENT.md) lists which plugin needs which.
|
|
51
228
|
|
|
52
|
-
|
|
53
|
-
---------------
|
|
229
|
+
### From a checkout
|
|
54
230
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
of associated plug-ins.
|
|
231
|
+
Use a checkout to try the current development version, change the source,
|
|
232
|
+
develop a plugin or verify changes before a release. The supported checkout setups are shown below; start with the minimal setup.
|
|
58
233
|
|
|
59
|
-
|
|
234
|
+
```sh
|
|
235
|
+
git clone https://github.com/id774/automaticruby.git
|
|
236
|
+
cd automaticruby
|
|
60
237
|
|
|
61
|
-
|
|
238
|
+
# Minimal: the framework and its test suite. No optional plugin gem.
|
|
239
|
+
bundle install
|
|
62
240
|
|
|
63
|
-
|
|
241
|
+
# All supported optional plugin dependencies, for plugin work.
|
|
242
|
+
bundle config set --local with plugins
|
|
243
|
+
bundle install
|
|
64
244
|
|
|
65
|
-
|
|
245
|
+
# Or start minimal and add one group at a time, as you use its plugins.
|
|
246
|
+
bundle config set --local with store
|
|
247
|
+
bundle install
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
```sh
|
|
251
|
+
bundle exec bin/automatic --version
|
|
252
|
+
bundle exec rake
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
A plain `bundle install` resolves the runtime dependencies declared by
|
|
256
|
+
`automatic.gemspec` and the development ones, and installs no optional plugin
|
|
257
|
+
gem: those are optional Bundler groups, which are installed only when asked
|
|
258
|
+
for. If the `bundle` command is unavailable, install Bundler first with
|
|
259
|
+
`gem install bundler`.
|
|
260
|
+
|
|
261
|
+
In a checkout, every `automatic` below becomes `bundle exec bin/automatic`, and
|
|
262
|
+
the checkout resolves its gems through the bundle — so a plugin's gem is added
|
|
263
|
+
with a group rather than with `gem install`, and a Recipe using plugins from two
|
|
264
|
+
groups selects both at once. The group names, which plugin needs which gem, and
|
|
265
|
+
a Recipe taken step by step through choosing its groups are in
|
|
266
|
+
[`doc/DEPLOYMENT.md`](doc/DEPLOYMENT.md); what each plugin does is in
|
|
267
|
+
[`doc/PLUGINS.md`](doc/PLUGINS.md).
|
|
268
|
+
|
|
269
|
+
## 6. Quick start
|
|
270
|
+
|
|
271
|
+
The complete first-run guide is [`doc/QUICKSTART.md`](doc/QUICKSTART.md): it
|
|
272
|
+
writes one Recipe that reads the public index pages listed by that guide,
|
|
273
|
+
installs what that Recipe needs, and leaves the new articles in a Markdown
|
|
274
|
+
document.
|
|
275
|
+
|
|
276
|
+
```sh
|
|
277
|
+
automatic scaffold
|
|
278
|
+
automatic -c ~/.automatic/config/example/feed2markdown.yml
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
`scaffold` creates `~/.automatic` with `config/`, `plugins/`, `db/` and
|
|
282
|
+
`assets/`, and copies the example Recipes into `~/.automatic/config/example`.
|
|
283
|
+
It never overwrites anything already there.
|
|
284
|
+
|
|
285
|
+
`feed2markdown.yml` is the shortest shipped Recipe: it fetches the public Ruby
|
|
286
|
+
news feed and appends its items to `~/.automatic/markdown/feeds.md`, using
|
|
287
|
+
nothing but the framework and what `gem install automatic` brought.
|
|
288
|
+
`feed2console.yml` beside it is the same pipeline printing to the terminal. Read
|
|
289
|
+
the file, `grep` it, put it in a repository, or hand it to whatever reads text
|
|
290
|
+
next. A Recipe that names a plugin with an optional gem installs that gem first,
|
|
291
|
+
which is covered by the "Install what the Recipe needs" section of
|
|
292
|
+
[`doc/QUICKSTART.md`](doc/QUICKSTART.md) and is the habit worth learning
|
|
293
|
+
early.
|
|
294
|
+
|
|
295
|
+
To check the framework without any network, write this instead:
|
|
296
|
+
|
|
297
|
+
```yaml
|
|
298
|
+
# ~/.automatic/config/selftest.yml
|
|
66
299
|
plugins:
|
|
67
|
-
- module:
|
|
300
|
+
- module: SubscriptionText
|
|
68
301
|
config:
|
|
69
|
-
|
|
302
|
+
feeds:
|
|
303
|
+
- title: hello
|
|
304
|
+
url: https://example.com/
|
|
305
|
+
- module: PublishMarkdown
|
|
70
306
|
```
|
|
71
307
|
|
|
72
|
-
|
|
308
|
+
```sh
|
|
309
|
+
automatic -c selftest.yml
|
|
310
|
+
```
|
|
73
311
|
|
|
312
|
+
A bare name is resolved inside `~/.automatic/config`, which is why that command
|
|
313
|
+
has no path in it. Anything with a `/` is used as a path as given.
|
|
74
314
|
|
|
75
|
-
|
|
76
|
-
-----------
|
|
315
|
+
Installing, scheduling and operating it is [`doc/DEPLOYMENT.md`](doc/DEPLOYMENT.md).
|
|
77
316
|
|
|
78
|
-
|
|
317
|
+
## 7. Recipes
|
|
79
318
|
|
|
319
|
+
A Recipe is one job: which plugins run, in what order, with what settings.
|
|
80
320
|
|
|
81
|
-
|
|
82
|
-
|
|
321
|
+
```yaml
|
|
322
|
+
global: # optional
|
|
323
|
+
log:
|
|
324
|
+
level: info # info | warn | error | none
|
|
83
325
|
|
|
84
|
-
|
|
326
|
+
plugins: # required
|
|
327
|
+
- module: SubscriptionFeed # required: the plugin class name
|
|
328
|
+
config: # optional: passed to that plugin only
|
|
329
|
+
feeds:
|
|
330
|
+
- https://example.com/feed
|
|
331
|
+
retry: 3
|
|
332
|
+
interval: 5
|
|
85
333
|
|
|
86
|
-
|
|
334
|
+
- module: PublishConsole # a plugin needing no settings omits config
|
|
335
|
+
```
|
|
87
336
|
|
|
88
|
-
|
|
337
|
+
- Entries run **in the order written**, each receiving the previous one's
|
|
338
|
+
output.
|
|
339
|
+
- `config` is handed to the plugin untouched. The framework does not validate
|
|
340
|
+
it and does not know what any key means.
|
|
341
|
+
- A `module` that resolves to nothing fails **before any plugin runs**, so a
|
|
342
|
+
typo costs nothing.
|
|
343
|
+
- `global.log.level` is the only framework setting. `global.timezone` and
|
|
344
|
+
`global.cache` appear in old Recipes and are read by nothing.
|
|
345
|
+
|
|
346
|
+
Conventions worth knowing before writing one:
|
|
347
|
+
|
|
348
|
+
- **Put a store plugin in front of anything with an effect.** `StorePermalink`
|
|
349
|
+
records what has been seen and passes on only what has not. It is what makes a
|
|
350
|
+
Recipe safe to run every hour instead of once.
|
|
351
|
+
- **Set `interval` on anything that fetches repeatedly**, in seconds. Scraping
|
|
352
|
+
politely is a requirement of this project, not a courtesy.
|
|
353
|
+
|
|
354
|
+
The full specification — every key, every failure mode, the type conventions,
|
|
355
|
+
and the compatibility promise — is [`doc/PLUGINS.md`](doc/PLUGINS.md) section 2.
|
|
356
|
+
|
|
357
|
+
## 8. Plugins
|
|
358
|
+
|
|
359
|
+
A plugin is a class in `Automatic::Plugin`, built with two arguments, answering
|
|
360
|
+
one method:
|
|
361
|
+
|
|
362
|
+
```ruby
|
|
363
|
+
module Automatic::Plugin
|
|
364
|
+
class FilterShortTitle
|
|
365
|
+
def initialize(config, pipeline = [])
|
|
366
|
+
@config = config || {}
|
|
367
|
+
@pipeline = pipeline
|
|
368
|
+
@max = (@config['max_length'] || 40).to_i
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
def run
|
|
372
|
+
@pipeline.each_with_object([]) do |feeds, returned|
|
|
373
|
+
next if feeds.nil?
|
|
374
|
+
|
|
375
|
+
kept = feeds.items.select { |item| item.title.to_s.length <= @max }
|
|
376
|
+
returned << Automatic::FeedMaker.create_pipeline(kept) unless kept.empty?
|
|
377
|
+
end
|
|
378
|
+
end
|
|
379
|
+
end
|
|
380
|
+
end
|
|
381
|
+
```
|
|
89
382
|
|
|
90
|
-
|
|
383
|
+
Save it as `~/.automatic/plugins/filter/short_title.rb` and a Recipe can name
|
|
384
|
+
`FilterShortTitle`. Nothing was registered: the class name and the file path are
|
|
385
|
+
the same fact written twice, and the loader converts between them.
|
|
386
|
+
|
|
387
|
+
For a smaller complete example, including testing guidance, see
|
|
388
|
+
[`doc/PLUGIN_DEVELOPMENT.md`](doc/PLUGIN_DEVELOPMENT.md).
|
|
389
|
+
|
|
390
|
+
| Category | Directory | Role |
|
|
391
|
+
| --- | --- | --- |
|
|
392
|
+
| `Subscription` | `subscription/` | Acquire from outside |
|
|
393
|
+
| `CustomFeed` | `custom_feed/` | Build a feed from a source that is not one |
|
|
394
|
+
| `Filter` | `filter/` | Select, reorder, rewrite |
|
|
395
|
+
| `Store` | `store/` | Persist, and drop what was seen before |
|
|
396
|
+
| `Provide` | `provide/` | Emit the payload elsewhere |
|
|
397
|
+
| `Notify` | `notify/` | Send a notification |
|
|
398
|
+
| `Publish` | `publish/` | Send the result out, print it, or write it as a document |
|
|
399
|
+
|
|
400
|
+
`~/.automatic/plugins` is searched **before** the installation, so a file named
|
|
401
|
+
like a shipped plugin replaces it.
|
|
402
|
+
|
|
403
|
+
### Which plugins still work
|
|
404
|
+
|
|
405
|
+
The complete shipped-plugin catalogue and the current status of each plugin
|
|
406
|
+
are maintained in
|
|
407
|
+
[`doc/PLUGINS.md`](doc/PLUGINS.md#6-the-plugins).
|
|
408
|
+
The status vocabulary and its meaning are defined in
|
|
409
|
+
[Reading the catalogue](doc/PLUGINS.md#5-reading-the-catalogue), and removed
|
|
410
|
+
integrations and the reasons for their removal are recorded in
|
|
411
|
+
[Plugins that were removed](doc/PLUGINS.md#8-plugins-that-were-removed).
|
|
412
|
+
|
|
413
|
+
This README intentionally does not repeat current plugin totals, per-status
|
|
414
|
+
totals, or the current membership of a status. Adding, removing, or
|
|
415
|
+
reclassifying a plugin changes the canonical catalogue rather than a
|
|
416
|
+
synchronized summary here.
|
|
417
|
+
|
|
418
|
+
A plugin classified as **Needs rework** in the catalogue is a
|
|
419
|
+
self-contained contribution candidate.
|
|
420
|
+
|
|
421
|
+
No plugin here is stubbed, mocked or simulated to make a test pass. Where a
|
|
422
|
+
plugin's gem is not installed its spec is skipped and says which gem is
|
|
423
|
+
missing; where the plugin still loads, its spec covers what does not need the
|
|
424
|
+
service. A dead integration is never made to look alive — it is removed.
|
|
425
|
+
|
|
426
|
+
The contract, a worked example, and how to test a plugin are in
|
|
427
|
+
[`doc/PLUGINS.md`](doc/PLUGINS.md) sections 3 and 4.
|
|
428
|
+
|
|
429
|
+
## 9. CLI usage
|
|
430
|
+
|
|
431
|
+
```sh
|
|
432
|
+
automatic -c RECIPE # run a Recipe
|
|
433
|
+
automatic SUBCOMMAND [ARGS] # run an auxiliary tool
|
|
434
|
+
automatic --help
|
|
435
|
+
automatic --version
|
|
436
|
+
```
|
|
91
437
|
|
|
92
|
-
|
|
438
|
+
### Options
|
|
439
|
+
|
|
440
|
+
| Option | Meaning |
|
|
441
|
+
| --- | --- |
|
|
442
|
+
| `-c`, `--config FILE` | The Recipe to run. A bare name is looked for in `~/.automatic/config`. |
|
|
443
|
+
| `-h`, `--help` | Print usage and exit `0`. |
|
|
444
|
+
| `-v`, `--version` | Print the version and exit `0`. |
|
|
445
|
+
|
|
446
|
+
### Subcommands
|
|
447
|
+
|
|
448
|
+
| Subcommand | What it does |
|
|
449
|
+
| --- | --- |
|
|
450
|
+
| `scaffold` | Create `~/.automatic` and its subdirectories. Overwrites nothing. |
|
|
451
|
+
| `unscaffold` | Remove `~/.automatic` entirely, **including your Recipes and databases**. |
|
|
452
|
+
| `autodiscovery <url>` | Print the feed URLs a page advertises. |
|
|
453
|
+
| `feedparser <url>` | Parse a feed and print the result. |
|
|
454
|
+
| `inspect <url>` | Discover a page's feeds, then parse the first. |
|
|
455
|
+
| `opmlparser <path>` | Print the feed URLs in an OPML file. |
|
|
456
|
+
| `log <level> <message>` | Emit one line in the framework's log format. |
|
|
457
|
+
|
|
458
|
+
The discovery and parsing subcommands help answer "will this work as a Recipe
|
|
459
|
+
input?" before you write the Recipe:
|
|
460
|
+
|
|
461
|
+
```sh
|
|
462
|
+
automatic autodiscovery https://example.com/
|
|
463
|
+
automatic inspect https://example.com/
|
|
464
|
+
automatic opmlparser subscriptions.opml > feeds.txt
|
|
465
|
+
```
|
|
93
466
|
|
|
94
|
-
|
|
467
|
+
### Exit status
|
|
95
468
|
|
|
96
|
-
|
|
469
|
+
| Status | Meaning |
|
|
470
|
+
| --- | --- |
|
|
471
|
+
| `0` | The Recipe ran, the subcommand did its work, or help or version was printed |
|
|
472
|
+
| `1` | The run or the subcommand failed, or nothing was asked for |
|
|
473
|
+
| `2` | The command line was rejected |
|
|
97
474
|
|
|
98
|
-
|
|
475
|
+
A `cron` entry can rely on these.
|
|
99
476
|
|
|
100
|
-
|
|
477
|
+
## 10. Configuration
|
|
101
478
|
|
|
102
|
-
|
|
479
|
+
There is no configuration file besides the Recipe. Every setting a job needs is
|
|
480
|
+
in the Recipe that defines the job, which is what makes a Recipe portable
|
|
481
|
+
between machines.
|
|
103
482
|
|
|
104
|
-
|
|
483
|
+
**Framework setting:** `global.log.level`, with the values `info`, `warn`,
|
|
484
|
+
`error` and `none`. Its Recipe-level contract is specified in
|
|
485
|
+
[`doc/PLUGINS.md`](doc/PLUGINS.md#24-global).
|
|
105
486
|
|
|
487
|
+
**Plugin settings** — the `config` mapping of a plugin entry, passed to that
|
|
488
|
+
plugin and read by nothing else. Established names: `retry` for an attempt
|
|
489
|
+
count, `interval` for seconds between attempts, `db` for a database file,
|
|
490
|
+
`path` for a directory.
|
|
106
491
|
|
|
107
|
-
|
|
108
|
-
|
|
492
|
+
**Credentials** are plugin settings, which makes a Recipe holding one a secret
|
|
493
|
+
file:
|
|
494
|
+
|
|
495
|
+
```sh
|
|
496
|
+
chmod 600 ~/.automatic/config/publish.yml
|
|
497
|
+
```
|
|
109
498
|
|
|
110
|
-
|
|
499
|
+
Nothing encrypts it and nothing keeps it elsewhere. Keep credentials in their
|
|
500
|
+
own Recipe, and never commit one. This is a weakness inherited from the original
|
|
501
|
+
design and is recorded as one in
|
|
502
|
+
[`doc/REQUIREMENTS.md`](doc/REQUIREMENTS.md) section 17.
|
|
111
503
|
|
|
504
|
+
**A Recipe is trusted local configuration.** It names Ruby classes and the
|
|
505
|
+
framework runs them, so anyone who can write a Recipe — or a file under
|
|
506
|
+
`~/.automatic/plugins` — can run code as you. Do not run a Recipe from a source
|
|
507
|
+
you do not trust. (Recipes are parsed with `YAML.safe_load` so that the document
|
|
508
|
+
itself cannot instantiate arbitrary Ruby classes, but that is a second line of
|
|
509
|
+
defence, not the boundary.)
|
|
112
510
|
|
|
113
|
-
|
|
114
|
-
----------
|
|
511
|
+
## 11. The user directory
|
|
115
512
|
|
|
116
|
-
|
|
513
|
+
`~/.automatic` holds what is yours, so that it survives reinstalling the gem.
|
|
117
514
|
|
|
118
|
-
|
|
515
|
+
| Path | Holds |
|
|
516
|
+
| --- | --- |
|
|
517
|
+
| `~/.automatic/config` | Your Recipes. A bare `-c` name is resolved here. |
|
|
518
|
+
| `~/.automatic/plugins` | Your plugins, in category subdirectories. Searched first. |
|
|
519
|
+
| `~/.automatic/db` | SQLite databases the store plugins write. |
|
|
520
|
+
| `~/.automatic/assets` | Data files plugins read, such as the fulltext siteinfo. |
|
|
521
|
+
|
|
522
|
+
Each part is optional; where one is absent, the corresponding directory inside
|
|
523
|
+
the installation is used instead. `automatic scaffold` creates and seeds them
|
|
524
|
+
and overwrites nothing, so it is safe to run after an upgrade.
|
|
525
|
+
|
|
526
|
+
## 12. Testing
|
|
527
|
+
|
|
528
|
+
```sh
|
|
529
|
+
bundle exec rake # the whole suite
|
|
530
|
+
bundle exec rake spec:lib # the framework only
|
|
531
|
+
bundle exec rake spec:plugins
|
|
532
|
+
COVERAGE=on bundle exec rake spec
|
|
533
|
+
```
|
|
534
|
+
|
|
535
|
+
- **The suite reaches no network and needs no credential.** That is a rule, not
|
|
536
|
+
a coincidence, and CI configures no secret.
|
|
537
|
+
- Specs mirror the source tree: `spec/lib/` for the framework,
|
|
538
|
+
`spec/plugins/<category>/` for plugins.
|
|
539
|
+
- Examples tagged `:network` reach real hosts and are **excluded by default**.
|
|
540
|
+
Several point at hosts that no longer serve what they expect, which is why
|
|
541
|
+
they are not a gate. Run them deliberately:
|
|
542
|
+
|
|
543
|
+
```sh
|
|
544
|
+
AUTOMATIC_NETWORK_SPECS=1 bundle exec rake spec
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
- A plugin whose gem the Gemfile declares in an optional group is **not
|
|
548
|
+
verified by the default suite**, because no optional group is installed.
|
|
549
|
+
Install them to run those specs as part of the ordinary suite:
|
|
550
|
+
|
|
551
|
+
```sh
|
|
552
|
+
bundle config set --local with plugins
|
|
553
|
+
bundle install
|
|
554
|
+
bundle exec rake
|
|
555
|
+
```
|
|
556
|
+
|
|
557
|
+
- A spec whose plugin needs a gem that is not installed is skipped, and says
|
|
558
|
+
which gem is missing. That absence is the signal; a plugin whose service no
|
|
559
|
+
longer exists is never stubbed into passing.
|
|
560
|
+
- `test/integration/` holds Recipes for exercising plugins against real
|
|
561
|
+
services. They are run by hand, are not part of the suite, and are never run
|
|
562
|
+
in CI. Most need a credential or a service you run — read one before
|
|
563
|
+
running it.
|
|
564
|
+
|
|
565
|
+
The required check installs the bundle, builds the gem, loads the library, runs
|
|
566
|
+
the CLI and runs the default suite on each validated Ruby version, from
|
|
567
|
+
[`.github/workflows/ci.yml`](.github/workflows/ci.yml). It configures no secret
|
|
568
|
+
and installs no optional plugin gem, so no plugin's own dependency is a
|
|
569
|
+
condition of a change being merged — and what it proves on every commit is that
|
|
570
|
+
the framework needs nothing but its own runtime dependencies. A separate,
|
|
571
|
+
non-required workflow,
|
|
572
|
+
[`.github/workflows/plugins.yml`](.github/workflows/plugins.yml), installs the
|
|
573
|
+
`plugins` group and runs the same suite, which is how the all-plugins setup is
|
|
574
|
+
checked.
|
|
575
|
+
|
|
576
|
+
## 13. Development
|
|
577
|
+
|
|
578
|
+
```sh
|
|
579
|
+
git clone https://github.com/id774/automaticruby.git
|
|
580
|
+
cd automaticruby
|
|
581
|
+
bundle install
|
|
582
|
+
bundle exec rake
|
|
583
|
+
bundle exec bin/automatic -c config/feed2console.yml
|
|
584
|
+
```
|
|
585
|
+
|
|
586
|
+
Contributions are welcome — a new plugin, or restoring a plugin currently
|
|
587
|
+
classified as **Needs rework** in the canonical catalogue, most of all.
|
|
588
|
+
|
|
589
|
+
1. Fork the repository.
|
|
590
|
+
2. Write the change, with a spec that reaches no network.
|
|
591
|
+
3. Update the documents in the same commit. A behaviour change with no
|
|
592
|
+
documentation change is not finished.
|
|
593
|
+
4. Send a pull request.
|
|
594
|
+
|
|
595
|
+
Read [`doc/POLICY.md`](doc/POLICY.md) first. It states the rules a change is
|
|
596
|
+
judged by: the direction of dependency, where a new capability belongs, how
|
|
597
|
+
dependencies are added, how the documents divide, and how a version history
|
|
598
|
+
entry is written.
|
|
599
|
+
|
|
600
|
+
Rules worth knowing before you start:
|
|
601
|
+
|
|
602
|
+
- **A gem needed by one plugin is not a dependency of the framework.** Require
|
|
603
|
+
it at the top of the plugin's own file.
|
|
604
|
+
- **Nothing dead is faked.** A plugin whose service has shut down is removed,
|
|
605
|
+
not stubbed into passing a test.
|
|
606
|
+
|
|
607
|
+
- Repository: <https://github.com/id774/automaticruby>
|
|
608
|
+
- Issues: <https://github.com/id774/automaticruby/issues>
|
|
609
|
+
- RubyGems: <https://rubygems.org/gems/automatic>
|
|
610
|
+
|
|
611
|
+
## 14. Repository structure
|
|
612
|
+
|
|
613
|
+
```text
|
|
614
|
+
.
|
|
615
|
+
├── bin/automatic The executable. A process entry point and nothing else.
|
|
616
|
+
├── lib/
|
|
617
|
+
│ ├── automatic.rb The module: directories, and run
|
|
618
|
+
│ └── automatic/
|
|
619
|
+
│ ├── cli.rb Options and subcommands; returns an exit status
|
|
620
|
+
│ ├── recipe.rb Loads and validates a Recipe
|
|
621
|
+
│ ├── pipeline.rb Finds plugins by name and runs them in order
|
|
622
|
+
│ ├── log.rb Levelled logging to standard output
|
|
623
|
+
│ ├── feed_maker.rb Builds pipeline values from plain data
|
|
624
|
+
│ ├── feed_parser.rb Fetches and parses feeds
|
|
625
|
+
│ ├── http.rb The one way in for what plugins fetch
|
|
626
|
+
│ ├── opml.rb OPML parser, for the opmlparser subcommand
|
|
627
|
+
│ ├── environment.rb Bundler setup for a source checkout
|
|
628
|
+
│ └── version.rb
|
|
629
|
+
├── plugins/ The shipped plugins, one directory per category
|
|
630
|
+
│ ├── subscription/ Acquire from outside
|
|
631
|
+
│ ├── custom_feed/ Build a feed from a source that is not one
|
|
632
|
+
│ ├── filter/ Select, reorder, rewrite
|
|
633
|
+
│ ├── store/ Persist, and drop what was seen before
|
|
634
|
+
│ ├── provide/ Emit the payload elsewhere
|
|
635
|
+
│ ├── notify/ Send a notification
|
|
636
|
+
│ └── publish/ Send the result out, print it, or write it as a document
|
|
637
|
+
├── config/ Example Recipes; scaffold copies these out
|
|
638
|
+
├── assets/siteinfo/ Data files plugins read
|
|
639
|
+
├── db/ Fallback for SQLite files when ~/.automatic/db is absent
|
|
640
|
+
├── spec/ RSpec suite, mirroring lib/ and plugins/
|
|
641
|
+
├── test/
|
|
642
|
+
│ ├── fixtures/ Fixtures for the manual tests
|
|
643
|
+
│ └── integration/ Recipes run by hand against real services
|
|
644
|
+
├── script/build Runs what CI runs, plus the integration recipes
|
|
645
|
+
├── vendor/ Legacy placeholder; the normal setup does not install gems here
|
|
646
|
+
├── doc/ See below
|
|
647
|
+
├── automatic.gemspec Hand-maintained
|
|
648
|
+
├── Gemfile
|
|
649
|
+
├── Rakefile
|
|
650
|
+
└── VERSION
|
|
651
|
+
```
|
|
652
|
+
|
|
653
|
+
## 15. Documents
|
|
654
|
+
|
|
655
|
+
Everything needed to understand, build, run and change this repository is in
|
|
656
|
+
this repository. No document here defers to another repository.
|
|
657
|
+
|
|
658
|
+
| Document | What it holds |
|
|
659
|
+
| --- | --- |
|
|
660
|
+
| [`doc/QUICKSTART.md`](doc/QUICKSTART.md) | The shortest path from installation to a Markdown result |
|
|
661
|
+
| [`doc/AI_TUTORIAL.md`](doc/AI_TUTORIAL.md) | Building a Recipe that collects web articles and asks an AI service one question about them |
|
|
662
|
+
| [`doc/PLUGIN_DEVELOPMENT.md`](doc/PLUGIN_DEVELOPMENT.md) | A complete user plugin and practical testing guidance |
|
|
663
|
+
| [`doc/REQUIREMENTS.md`](doc/REQUIREMENTS.md) | What the system is for, what it guarantees, where its responsibility ends |
|
|
664
|
+
| [`doc/BASIC_DESIGN.md`](doc/BASIC_DESIGN.md) | How it is composed: the parts, their responsibilities, the flow of a run |
|
|
665
|
+
| [`doc/PLUGINS.md`](doc/PLUGINS.md) | The Recipe format, the plugin contract, and the catalogue of every shipped plugin |
|
|
666
|
+
| [`doc/POLICY.md`](doc/POLICY.md) | How a change is made and judged: style, dependencies, tests, versioning |
|
|
667
|
+
| [`doc/DEPLOYMENT.md`](doc/DEPLOYMENT.md) | Installing, scheduling, operating, and what to do when it fails |
|
|
668
|
+
| [`doc/RELEASING.md`](doc/RELEASING.md) | For maintainers: building, verifying and publishing the gem |
|
|
669
|
+
| [`doc/VERSIONS`](doc/VERSIONS) | The release history, from 2012 |
|
|
670
|
+
| [`doc/LICENSE.md`](doc/LICENSE.md) | The licence |
|
|
671
|
+
| [`doc/COPYING`](doc/COPYING) | The GPLv3 text |
|
|
672
|
+
| [`doc/COPYING.LESSER`](doc/COPYING.LESSER) | The LGPLv3 text |
|
|
673
|
+
| [`doc/AUTHORS`](doc/AUTHORS) | Contributors |
|
|
674
|
+
|
|
675
|
+
## 16. Versioning
|
|
676
|
+
|
|
677
|
+
Releases are numbered `<year>.<month>`, two digits each, taken from the release
|
|
678
|
+
date. The scheme mimics Ubuntu's and has been used since the first release in
|
|
679
|
+
February 2012.
|
|
680
|
+
|
|
681
|
+
```text
|
|
682
|
+
26.08 a release made in August 2026
|
|
683
|
+
26.08.1 a release correcting 26.08 in the same month
|
|
684
|
+
```
|
|
119
685
|
|
|
120
|
-
|
|
686
|
+
A patch release may correct an earlier release in the same month, but a
|
|
687
|
+
versioned release is never made twice on the same calendar date.
|
|
121
688
|
|
|
689
|
+
The number carries no compatibility meaning: a month is not a major version. A
|
|
690
|
+
change that affects an existing Recipe is stated as such in its
|
|
691
|
+
[`doc/VERSIONS`](doc/VERSIONS) entry.
|
|
122
692
|
|
|
123
|
-
|
|
124
|
-
|
|
693
|
+
What this repository promises not to break — the Recipe format, the plugin
|
|
694
|
+
contract, the plugin naming rule, the user directory, the CLI, and existing
|
|
695
|
+
store databases — is listed in [`doc/POLICY.md`](doc/POLICY.md) section 7.
|
|
125
696
|
|
|
126
|
-
|
|
697
|
+
## 17. Name
|
|
127
698
|
|
|
128
|
-
|
|
699
|
+
The name **Automatic Ruby** was partly inspired by David Heinemeier Hansson's
|
|
700
|
+
story of naming Ruby on Rails. He first chose *Rails* for the idea of putting
|
|
701
|
+
development "on rails"; when the obvious Rails domain names were unavailable,
|
|
702
|
+
`rubyonrails.com` was free. He later said he liked the result because it put
|
|
703
|
+
Ruby itself more prominently in the name and helped introduce Ruby to a wider
|
|
704
|
+
audience. See [his account of the naming](https://tim.blog/2018/06/05/the-tim-ferriss-show-transcripts-david-dhh-heinemeier-hansson/).
|
|
129
705
|
|
|
130
|
-
|
|
706
|
+
I took a simple lesson from that story: a software name does not have to
|
|
707
|
+
literally describe its architecture. This project was inspired in part by
|
|
708
|
+
Plagger, and I wanted the name to put two things up front: **automatic**
|
|
709
|
+
processing, and doing it in **Ruby** rather than Perl. *Automatic Ruby* said
|
|
710
|
+
both, sounded natural enough, and was easy to remember. That was enough.
|
|
131
711
|
|
|
132
|
-
|
|
133
|
-
+ http://github.com/id774
|
|
712
|
+
## 18. Primary Branch
|
|
134
713
|
|
|
714
|
+
This repository uses `master` as its primary branch name.
|
|
135
715
|
|
|
136
|
-
|
|
137
|
-
-------
|
|
716
|
+
The name is used solely as a technical identifier, following the long-standing convention historically used by Git. It does not express or imply any association with racism, slavery, discrimination, or any political or social ideology.
|
|
138
717
|
|
|
139
|
-
|
|
718
|
+
## 19. License
|
|
140
719
|
|
|
141
|
-
|
|
720
|
+
Automatic Ruby is dual-licensed under the
|
|
721
|
+
[GNU General Public License, Version 3](https://www.gnu.org/licenses/gpl-3.0.html)
|
|
722
|
+
or the
|
|
723
|
+
[GNU Lesser General Public License, Version 3](https://www.gnu.org/licenses/lgpl-3.0.html).
|
|
724
|
+
You may choose either license at your discretion.
|
|
142
725
|
|
|
143
|
-
|
|
144
|
-
|
|
726
|
+
See [`doc/LICENSE.md`](doc/LICENSE.md), [`doc/COPYING`](doc/COPYING) and
|
|
727
|
+
[`doc/COPYING.LESSER`](doc/COPYING.LESSER) for the full license texts.
|
|
145
728
|
|
|
729
|
+
Copyright (c) 2012-2026 Automatic Ruby Developers.
|
|
146
730
|
|
|
731
|
+
Project created by [id774](https://id774.net). Contributors are listed in
|
|
732
|
+
[`doc/AUTHORS`](doc/AUTHORS).
|