automatic 14.12.2 → 26.08

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (172) hide show
  1. checksums.yaml +5 -5
  2. data/README.md +635 -83
  3. data/VERSION +1 -1
  4. data/automatic.gemspec +109 -248
  5. data/bin/automatic +20 -139
  6. data/config/feed2console.yml +10 -4
  7. data/config/feed2markdown.yml +41 -0
  8. data/doc/AI_TUTORIAL.md +518 -0
  9. data/doc/BASIC_DESIGN.md +516 -0
  10. data/doc/COPYING.LESSER +165 -0
  11. data/doc/DEPLOYMENT.md +824 -0
  12. data/doc/LICENSE.md +14 -0
  13. data/doc/PLUGINS.md +1875 -0
  14. data/doc/PLUGIN_DEVELOPMENT.md +86 -0
  15. data/doc/POLICY.md +857 -0
  16. data/doc/QUICKSTART.md +256 -0
  17. data/doc/RELEASING.md +381 -0
  18. data/doc/REQUIREMENTS.md +526 -0
  19. data/doc/VERSIONS +208 -0
  20. data/lib/automatic/cli.rb +248 -0
  21. data/lib/automatic/environment.rb +31 -5
  22. data/lib/automatic/feed_maker.rb +10 -9
  23. data/lib/automatic/feed_parser.rb +51 -35
  24. data/lib/automatic/http.rb +107 -0
  25. data/lib/automatic/log.rb +49 -18
  26. data/lib/automatic/opml.rb +3 -1
  27. data/lib/automatic/pipeline.rb +63 -32
  28. data/lib/automatic/recipe.rb +56 -17
  29. data/lib/automatic/version.rb +14 -1
  30. data/lib/automatic.rb +78 -20
  31. data/plugins/custom_feed/svn_log.rb +73 -32
  32. data/plugins/custom_feed/web.rb +348 -0
  33. data/plugins/filter/absolute_uri.rb +43 -27
  34. data/plugins/filter/accept.rb +38 -45
  35. data/plugins/filter/claude.rb +217 -0
  36. data/plugins/filter/clear.rb +12 -8
  37. data/plugins/filter/description_link.rb +49 -51
  38. data/plugins/filter/full_feed.rb +158 -52
  39. data/plugins/filter/gemini.rb +216 -0
  40. data/plugins/filter/github_feed.rb +38 -26
  41. data/plugins/filter/ignore.rb +33 -44
  42. data/plugins/filter/image.rb +36 -25
  43. data/plugins/filter/image_source.rb +58 -52
  44. data/plugins/filter/join.rb +107 -0
  45. data/plugins/filter/one.rb +19 -26
  46. data/plugins/filter/open_ai.rb +198 -0
  47. data/plugins/filter/rand.rb +16 -17
  48. data/plugins/filter/sakura_ai.rb +205 -0
  49. data/plugins/filter/sanitize.rb +29 -34
  50. data/plugins/filter/sort.rb +20 -27
  51. data/plugins/filter/tumblr_resize.rb +31 -23
  52. data/plugins/notify/ikachan.rb +86 -48
  53. data/plugins/provide/fluentd.rb +43 -24
  54. data/plugins/publish/amazon_s3.rb +73 -40
  55. data/plugins/publish/console.rb +19 -16
  56. data/plugins/publish/console_link.rb +20 -17
  57. data/plugins/publish/eject.rb +48 -26
  58. data/plugins/publish/fluentd.rb +50 -30
  59. data/plugins/publish/hatena_bookmark.rb +88 -71
  60. data/plugins/publish/instapaper.rb +69 -59
  61. data/plugins/publish/markdown.rb +278 -0
  62. data/plugins/publish/memcached.rb +35 -29
  63. data/plugins/store/database.rb +50 -48
  64. data/plugins/store/digest.rb +212 -0
  65. data/plugins/store/file.rb +99 -68
  66. data/plugins/store/full_text.rb +32 -25
  67. data/plugins/store/permalink.rb +18 -22
  68. data/plugins/subscription/feed.rb +34 -21
  69. data/plugins/subscription/link.rb +31 -32
  70. data/plugins/subscription/text.rb +32 -46
  71. data/plugins/subscription/tumblr.rb +55 -44
  72. data/plugins/subscription/xml.rb +40 -36
  73. metadata +108 -321
  74. data/Gemfile +0 -38
  75. data/Rakefile +0 -59
  76. data/doc/ChangeLog +0 -303
  77. data/doc/PLUGINS +0 -750
  78. data/doc/PLUGINS.ja +0 -753
  79. data/doc/README +0 -511
  80. data/doc/README.ja +0 -519
  81. data/plugins/filter/google_news.rb +0 -50
  82. data/plugins/publish/google_calendar.rb +0 -84
  83. data/plugins/publish/hipchat.rb +0 -46
  84. data/plugins/publish/pocket.rb +0 -45
  85. data/plugins/publish/twitter.rb +0 -58
  86. data/plugins/subscription/chan_toru.rb +0 -57
  87. data/plugins/subscription/g_guide.rb +0 -57
  88. data/plugins/subscription/pocket.rb +0 -51
  89. data/plugins/subscription/twitter.rb +0 -69
  90. data/plugins/subscription/twitter_search.rb +0 -50
  91. data/plugins/subscription/weather.rb +0 -33
  92. data/script/build +0 -84
  93. data/spec/fixtures/sampleFeeds.tsv +0 -1
  94. data/spec/fixtures/sampleFeeds2.tsv +0 -2
  95. data/spec/fixtures/sampleRecipe.yml +0 -24
  96. data/spec/lib/automatic/log_spec.rb +0 -32
  97. data/spec/lib/automatic/pipeline_spec.rb +0 -68
  98. data/spec/lib/automatic/recipe_spec.rb +0 -40
  99. data/spec/lib/automatic_spec.rb +0 -99
  100. data/spec/plugins/custom_feed/svn_log_spec.rb +0 -31
  101. data/spec/plugins/filter/absolute_uri_spec.rb +0 -61
  102. data/spec/plugins/filter/accept_spec.rb +0 -331
  103. data/spec/plugins/filter/clear_spec.rb +0 -49
  104. data/spec/plugins/filter/description_link_spec.rb +0 -138
  105. data/spec/plugins/filter/full_feed_spec.rb +0 -129
  106. data/spec/plugins/filter/github_feed_spec.rb +0 -55
  107. data/spec/plugins/filter/google_news_spec.rb +0 -69
  108. data/spec/plugins/filter/ignore_spec.rb +0 -328
  109. data/spec/plugins/filter/image_source_spec.rb +0 -89
  110. data/spec/plugins/filter/image_spec.rb +0 -65
  111. data/spec/plugins/filter/one_spec.rb +0 -71
  112. data/spec/plugins/filter/rand_spec.rb +0 -52
  113. data/spec/plugins/filter/sanitize_spec.rb +0 -153
  114. data/spec/plugins/filter/sort_spec.rb +0 -189
  115. data/spec/plugins/filter/tumblr_resize_spec.rb +0 -109
  116. data/spec/plugins/notify/ikachan_spec.rb +0 -58
  117. data/spec/plugins/provide/fluentd_spec.rb +0 -49
  118. data/spec/plugins/publish/amazon_s3_spec.rb +0 -40
  119. data/spec/plugins/publish/console_spec.rb +0 -30
  120. data/spec/plugins/publish/eject_spec.rb +0 -40
  121. data/spec/plugins/publish/fluentd_spec.rb +0 -40
  122. data/spec/plugins/publish/google_calendar_spec.rb +0 -83
  123. data/spec/plugins/publish/hatena_bookmark_spec.rb +0 -134
  124. data/spec/plugins/publish/hipchat_spec.rb +0 -69
  125. data/spec/plugins/publish/instapaper_spec.rb +0 -82
  126. data/spec/plugins/publish/memcached_spec.rb +0 -63
  127. data/spec/plugins/publish/pocket_spec.rb +0 -51
  128. data/spec/plugins/publish/twitter_spec.rb +0 -73
  129. data/spec/plugins/store/file_spec.rb +0 -58
  130. data/spec/plugins/store/full_text_spec.rb +0 -152
  131. data/spec/plugins/store/permalink_spec.rb +0 -206
  132. data/spec/plugins/subscription/chan_toru_spec.rb +0 -56
  133. data/spec/plugins/subscription/feed_spec.rb +0 -71
  134. data/spec/plugins/subscription/g_guide_spec.rb +0 -82
  135. data/spec/plugins/subscription/link_spec.rb +0 -72
  136. data/spec/plugins/subscription/pocket_spec.rb +0 -57
  137. data/spec/plugins/subscription/text_spec.rb +0 -84
  138. data/spec/plugins/subscription/tumblr_spec.rb +0 -74
  139. data/spec/plugins/subscription/twitter_search_spec.rb +0 -57
  140. data/spec/plugins/subscription/twitter_spec.rb +0 -73
  141. data/spec/plugins/subscription/weather_spec.rb +0 -44
  142. data/spec/plugins/subscription/xml_spec.rb +0 -84
  143. data/spec/spec_helper.rb +0 -106
  144. data/spec/user_dir/plugins/store/mock.rb +0 -16
  145. data/test/fixtures/sampleOPML.xml +0 -11
  146. data/test/integration/test_absoluteurl.yml +0 -25
  147. data/test/integration/test_activerecord.yml +0 -24
  148. data/test/integration/test_add_pocket.yml +0 -26
  149. data/test/integration/test_chan_toru.yml +0 -21
  150. data/test/integration/test_descriptionlink.yml +0 -21
  151. data/test/integration/test_fluentd.yml +0 -22
  152. data/test/integration/test_fulltext.yml +0 -30
  153. data/test/integration/test_google_news.yml +0 -21
  154. data/test/integration/test_googlealert.yml +0 -21
  155. data/test/integration/test_hatenabookmark.yml +0 -30
  156. data/test/integration/test_ignore.yml +0 -25
  157. data/test/integration/test_ignore2.yml +0 -22
  158. data/test/integration/test_image2local.yml +0 -33
  159. data/test/integration/test_instapaper.yml +0 -26
  160. data/test/integration/test_link2local.yml +0 -34
  161. data/test/integration/test_one.yml +0 -23
  162. data/test/integration/test_pocket.yml +0 -22
  163. data/test/integration/test_rand.yml +0 -21
  164. data/test/integration/test_sanitize.yml +0 -23
  165. data/test/integration/test_sort.yml +0 -36
  166. data/test/integration/test_svnlog.yml +0 -15
  167. data/test/integration/test_text2feed.yml +0 -36
  168. data/test/integration/test_tumblr2local.yml +0 -43
  169. data/test/integration/test_twitter_search.yml +0 -22
  170. data/test/integration/test_weather.yml +0 -19
  171. data/test/integration/test_xml2fluentd.yml +0 -21
  172. data/vendor/.gitkeep +0 -0
data/doc/POLICY.md ADDED
@@ -0,0 +1,857 @@
1
+ # Implementation Policies
2
+
3
+ This document decides how this repository is implemented: the coding rules, the
4
+ responsibilities of the parts and the direction of dependency between them, the
5
+ handling of settings and credentials, the approach to tests and documentation,
6
+ the versioning scheme, and the criteria a change is judged by.
7
+
8
+ It stands on its own. No rule here is completed by a document kept in another
9
+ repository, and a subject it does not cover is a gap in this document, to be
10
+ filled here rather than looked up elsewhere.
11
+
12
+ What the system is for belongs to [`REQUIREMENTS.md`](REQUIREMENTS.md); how it
13
+ is composed belongs to [`BASIC_DESIGN.md`](BASIC_DESIGN.md); the Recipe format
14
+ and the plugin contract belong to [`PLUGINS.md`](PLUGINS.md). This document does
15
+ not restate them; it decides how they are carried out.
16
+
17
+ The Invariants below decide over the rest of it.
18
+
19
+ ---
20
+
21
+ ## 1. General Policy
22
+
23
+ ### 1.1 Purpose and scope
24
+
25
+ - **Automatic Ruby is a framework, and the framework is the small part.** It
26
+ loads a Recipe, finds classes by name and calls them in order. Nearly every
27
+ change belongs in a plugin, and a change that adds domain knowledge to the
28
+ framework needs a reason beyond convenience.
29
+ - **It is one person's tooling, run unattended from `cron`.** It is not a
30
+ service, not multi-tenant and not a product. That premise decides several
31
+ rules below that would otherwise look lax — the Recipe being trusted, chiefly.
32
+ - This document applies to everything committed here: the library, the plugins,
33
+ the executable, the tests, the packaging and the documents.
34
+ - Ruby is the implementation language and stays the implementation language. A
35
+ proposal to reimplement the core in another language is out of scope, and so
36
+ is turning this into a Rails application.
37
+
38
+ ### 1.2 Invariants
39
+
40
+ These come before every other rule here. Some of what they forbid is what a
41
+ general policy would otherwise ask for.
42
+
43
+ 1. **The Recipe format and the plugin contract are interfaces, and this
44
+ repository is not free to change them.** Both are depended on by files this
45
+ repository cannot see: Recipes in `~/.automatic/config`, plugins in
46
+ `~/.automatic/plugins`. A change that breaks either is deliberate, is
47
+ justified, and is recorded in [`VERSIONS`](VERSIONS).
48
+ 2. **The pipeline value has one shape.** Every plugin takes and returns an array
49
+ of feed objects. Introducing a second representation would end composability,
50
+ which is the whole of what the framework provides.
51
+ 3. **The plugin architecture is not replaced.** Recipes are not superseded by
52
+ hard-coded procedures, the loader is not replaced by a registry that must be
53
+ edited to add a plugin, and the framework does not become a single
54
+ application with the plugins folded into it.
55
+ 4. **A dependency needed by one plugin is not a dependency of the framework.**
56
+ Installing the gem must not pull in an SDK for a service the operator does
57
+ not use. See section 9.
58
+ 5. **A failure is never silent.** No `rescue` that returns an empty pipeline as
59
+ if nothing happened, and no run that exits zero after something went wrong.
60
+ 6. **The default test suite reaches no network and needs no credential.** None
61
+ is configured in CI.
62
+ 7. **A plugin that cannot work is never simulated into working.** Stubbing a
63
+ dead service to make a test pass, or to make the catalogue look better, is
64
+ forbidden outright. Removing the plugin is the correct outcome. See
65
+ section 4.
66
+ 8. **No credential is committed**, in a Recipe, an example, a fixture or a test,
67
+ and none is written to the log.
68
+ 9. **Historical release history is not rewritten.** Past versions and their
69
+ dates in [`VERSIONS`](VERSIONS) are a record, not a thing to tidy.
70
+ 10. **The licence is GPL version 3 or LGPL version 3.** Automatic Ruby is
71
+ dual-licensed, and a user may choose either license at their discretion. New
72
+ files use the same dual license.
73
+
74
+ ### 1.3 Design philosophy
75
+
76
+ - Simple comes before clever. Common work should fit in a short Recipe, and a
77
+ new abstraction needs a concrete problem the existing plugin contract cannot
78
+ solve.
79
+ - Documentation examples must run, and the Quick Start uses Supported plugins
80
+ only. Supported paths receive maintenance priority over integrations that
81
+ need rework.
82
+
83
+ - Prefer the smallest change that solves the problem. A pipeline that has run in
84
+ someone's `cron` for a decade has earned the benefit of the doubt.
85
+ - Understand why something exists before replacing it. A constant that looks
86
+ arbitrary usually encodes an operational fact.
87
+ - A capability that can live in a plugin lives in a plugin.
88
+ - Where a decision looks odd, a comment gives the reason, so that a later change
89
+ does not quietly undo it.
90
+ - Old is not a defect. Neither is unfashionable. A defect is something that does
91
+ not work, is not understood, or cannot be tested.
92
+
93
+ ### 1.4 The parts and the direction of dependency
94
+
95
+ ```text
96
+ bin/automatic
97
+ |
98
+ v
99
+ lib/automatic/cli.rb
100
+ |
101
+ v
102
+ lib/automatic.rb -> recipe.rb -> pipeline.rb
103
+ | |
104
+ | v
105
+ | Automatic::Plugin::*
106
+ v |
107
+ log.rb feed_maker.rb feed_parser.rb http.rb
108
+ ^----------------------------------+
109
+ ```
110
+
111
+ Dependency points one way and there is no edge back up:
112
+
113
+ - **`bin/automatic` knows only the CLI.** It puts `lib` on the load path,
114
+ requires `automatic/cli`, and exits with the status it is given. No option, no
115
+ subcommand and no policy lives there.
116
+ - **The CLI knows the framework; the framework does not know the CLI.** Nothing
117
+ under `lib/automatic/` other than `cli.rb` may reference it.
118
+ - **`Pipeline` knows how to find and call a plugin; it knows no plugin.** A
119
+ reference to a plugin class name in the framework is a design error.
120
+ - **A plugin knows `Log`, `FeedMaker`, `FeedParser`, `Http` and its own
121
+ libraries.** It does not know another plugin, does not know the CLI, and does
122
+ not reach into `Automatic` for directories other than through the helpers
123
+ provided.
124
+ - **`Log`, `FeedMaker`, `FeedParser` and `Http` are leaves.** They depend on
125
+ nothing else in this repository.
126
+ - A new responsibility goes to the part that owns it. Where it appears to belong
127
+ to two, the boundary is wrong and is corrected, rather than the code being
128
+ written across it.
129
+
130
+ ### 1.5 The plugin boundary
131
+
132
+ - The framework's knowledge of a plugin is: its name, its file's location, its
133
+ constructor's two arguments and its `run` method. It does not know a plugin's
134
+ category semantics, its settings, its dependencies or its failure modes.
135
+ - The framework does not validate a plugin's `config`. It cannot know what is
136
+ valid, and pretending otherwise would put plugin knowledge in the framework.
137
+ - The framework does not inspect the pipeline between plugins.
138
+ - A plugin does not modify framework state. It does not set `Automatic.root_dir`
139
+ or `Automatic.user_dir`, and it does not change the log level.
140
+ - Plugin-to-plugin dependency is avoided. A plugin that wants another plugin's
141
+ result is a Recipe with two entries in it.
142
+ - Shared plugin code that the framework does not use stays under `plugins/`, not
143
+ in `lib/`. `plugins/store/database.rb` is where it is for that reason.
144
+
145
+ ### 1.6 Configuration
146
+
147
+ - A Recipe is the whole of a job's configuration. There is no second file, no
148
+ configuration directory, and no environment-variable settings.
149
+ - The one exception is `AUTOMATIC_RUBY_ENV=test`, which permits the user
150
+ directory to be overridden. It exists for the tests and is not an operator
151
+ interface.
152
+ - The framework reads exactly one `global` key, `global.log.level`. Adding a
153
+ second is a change to the Recipe format and is judged as one.
154
+ - `global.timezone` and `global.cache` are inert and stay inert. They are not
155
+ removed — that would edit operators' files for no gain — and they are not
156
+ given a meaning.
157
+ - A plugin reads its settings from `@config` by string key, tolerates `nil` for
158
+ the mapping and for any key, and follows the established names: `retry`,
159
+ `interval`, `db`, `path`.
160
+ - Not every constant becomes a setting. A value the operator would plausibly
161
+ change is a setting; a value that is part of what the plugin means stays in
162
+ the code.
163
+
164
+ ### 1.7 Error handling
165
+
166
+ - **The framework catches nothing from a plugin.** A plugin that raises ends the
167
+ run, for the reason given in `REQUIREMENTS.md` section 12. Adding a blanket
168
+ rescue around plugin execution would change what every existing Recipe means
169
+ and is not done.
170
+ - **A plugin owns its transient failures**, through `retry` and `interval`, and
171
+ logs each failed attempt.
172
+ - The framework's own failures have named classes in `lib/automatic.rb`:
173
+ `Automatic::Error` and, under it, `NoRecipeError`, `NoPluginError` and
174
+ `InvalidRecipeError`. A new framework failure gets a class rather than a
175
+ `RuntimeError` with a message.
176
+ - **`rescue` without a class is `rescue StandardError` and that is what is
177
+ meant.** A bare `rescue` is acceptable in a plugin's retry block, where the
178
+ point is that any failure of the attempt is retried, and unacceptable
179
+ elsewhere. Where used, it logs.
180
+ - An unexpected exception is left to propagate out of the CLI. It is a defect
181
+ and its backtrace is wanted.
182
+ - **The library never calls `exit` or `abort`.** Exit status is decided at the
183
+ process entry point, from the value `Automatic::CLI.run` returns.
184
+
185
+ ### 1.8 Logging and output
186
+
187
+ - **A library file never calls `puts`, `print` or `warn`.** It logs, through
188
+ `Automatic::Log`.
189
+ - The CLI writes diagnostics to standard error and requested output — help,
190
+ version, subcommand results — to standard output.
191
+ - A plugin whose purpose is to write to the terminal holds its output object in
192
+ an instance variable defaulting to `$stdout`, so that a test can substitute it.
193
+ - `info` says what a step did, `warn` says what was skipped, `error` says what
194
+ failed. An error that was rescued is logged at `warn` or `error`, never at
195
+ `info`.
196
+ - No log line contains a credential. A plugin that logs its own settings
197
+ wholesale is a defect.
198
+
199
+ ### 1.9 Filesystem access
200
+
201
+ - The framework touches two roots: the installation directory and
202
+ `~/.automatic`. Nothing else, and no absolute path elsewhere appears in the
203
+ code.
204
+ - Paths are built with `File.join` and `File.expand_path`, never by string
205
+ concatenation.
206
+ - The user directory takes precedence over the installation, for plugins,
207
+ Recipes, databases and assets alike. Where a part of it is absent, the
208
+ installation's own directory is the fallback.
209
+ - `scaffold` creates and never overwrites. `unscaffold` removes the whole user
210
+ directory and is the one destructive operation the framework offers; it is not
211
+ extended, and nothing else acquires the ability to delete an operator's data.
212
+ - A plugin writes only where its settings tell it to.
213
+
214
+ ### 1.10 Network access
215
+
216
+ - **The framework reaches nothing.** No update check, no telemetry, no
217
+ phone-home. Every request is a plugin's, on a Recipe's instruction.
218
+ - HTTPS is used wherever the service offers it. A plugin that still uses plain
219
+ HTTP is a defect to be recorded, and one that sends a credential over plain
220
+ HTTP is recorded as such in [`PLUGINS.md`](PLUGINS.md).
221
+ - **TLS certificate verification is never disabled.** There is no acceptable
222
+ reason, and a plugin that did it has been corrected.
223
+ - A plugin that fetches repeatedly from one host supports `interval` and its
224
+ documentation says to set it. Scraping politely is a requirement, not a
225
+ courtesy.
226
+ - A URL that comes from a setting or from feed content is escaped before use,
227
+ and is never interpolated into a shell command. An external command is run as
228
+ an argument vector.
229
+ - **A URL that comes from feed content is external input.** It is fetched
230
+ through `Automatic::Http`, which restricts the scheme to HTTP and HTTPS,
231
+ because `URI.open` on such a string will read a local file as readily as an
232
+ article.
233
+ - Every request has a connect and a read timeout. An unattended run that hangs
234
+ is a failure mode with no upper bound on its cost.
235
+
236
+ ### 1.11 Security and credentials
237
+
238
+ - **A Recipe is trusted local configuration**, equivalent to a shell script the
239
+ operator wrote. This is the trust boundary, it is stated in
240
+ `REQUIREMENTS.md` section 17, and it is the premise the following rules assume.
241
+ - Even so, YAML is loaded safely, so that a Recipe cannot name a Ruby class to
242
+ instantiate. This costs nothing, since no Recipe needs it, and it removes a
243
+ second and avoidable path to code execution. **Do not present this as making
244
+ an untrusted Recipe safe**; the loader is not the boundary.
245
+ - Credentials are Recipe settings. That makes a Recipe holding them a secret
246
+ file, which the documentation says plainly rather than glossing over.
247
+ - No credential in the repository: not in an example Recipe, not in a fixture,
248
+ not in a test, not in a comment. The example Recipes in `config/` need none.
249
+ - No credential in the log, in an exception message, or in a pipeline item.
250
+ - No credential in CI. A test that needs one is not part of the default suite.
251
+ - A change that touches authentication says in its `VERSIONS` entry what it
252
+ changed.
253
+
254
+ ### 1.12 Judging a change
255
+
256
+ - Does it keep the Recipe format and the plugin contract, and if not, is that
257
+ deliberate and recorded?
258
+ - Does it respect the direction of dependency?
259
+ - Does it leave the framework the small part?
260
+ - Does it keep a plugin's dependency out of everyone else's installation?
261
+ - Is it the smallest change that does the job?
262
+ - Does a test say it works, and does the default suite still need no network?
263
+ - Do the documents still match the code, in the same commit?
264
+
265
+ ---
266
+
267
+ ## 2. Ruby Policy
268
+
269
+ ### 2.1 Style
270
+
271
+ The prevailing style of this repository is what a change matches. It is not
272
+ current fashion and that is deliberate: a change written in a different style
273
+ makes a diff harder to read than the style saves.
274
+
275
+ - Two-space indentation. No tabs. No trailing whitespace. A newline at end of
276
+ file.
277
+ - Single quotes for a plain string, double quotes when interpolating.
278
+ - `snake_case` for methods and variables, `CamelCase` for classes, `SCREAMING_
279
+ SNAKE_CASE` for constants.
280
+ - Braces for blocks are the established idiom here, including multi-line blocks,
281
+ and were chosen to avoid stacked `end`s. Existing code is left as it is. New
282
+ code may use either; matching the surrounding file matters more than the
283
+ choice.
284
+ - Lines around 100 columns. Not enforced, and not a reason to reformat.
285
+ - Prefer a guard clause to a wrapping `unless`, in new code.
286
+
287
+ ### 2.2 Scope of a change
288
+
289
+ - **Do not reformat a file you are not otherwise changing.** A large diff hides
290
+ the change inside it.
291
+ - Do not rename a class, a method or a setting because the name is dated. The
292
+ names in this repository are domain vocabulary: Recipe, pipeline, plugin,
293
+ subscription, publish. They stay.
294
+ - Do not restructure a plugin while fixing it. A compatibility fix and a rewrite
295
+ are two changes.
296
+ - Where a file is being substantially changed anyway, bringing it to the current
297
+ style is fine.
298
+
299
+ ### 2.3 File headers
300
+
301
+ Every Ruby file carries a header comment. Values start in the same column,
302
+ using `Source Code::`, the longest label, as the alignment baseline. Fields
303
+ appear in the order shown below. An executable uses this canonical form:
304
+
305
+ ```ruby
306
+ #!/usr/bin/env ruby
307
+ # -*- coding: utf-8 -*-
308
+ # Name:: automatic
309
+ # Author: id774 (More info: http://id774.net)
310
+ # Source Code:: https://github.com/id774/automaticruby
311
+ # License:: The GPL version 3, or LGPL version 3 (Dual License).
312
+ # Contact:: idnanashi@gmail.com
313
+ # Created:: Feb 18, 2012
314
+ # Updated:: Aug 14, 2026
315
+ # Copyright:: Copyright (c) 2012-2026 Automatic Ruby Developers.
316
+ ```
317
+
318
+ - Only files directly invoked as executables carry the shebang. Libraries,
319
+ plugins and specs omit the shebang and otherwise use the same form.
320
+ - `Name` is the fully qualified name of what the file defines.
321
+ - `Author` records the author or authors established by the file and its Git
322
+ history. The former name `774` is normalized to `id774`; third-party authors
323
+ are never removed or replaced, and multiple authors retain their recorded
324
+ order. Names and URLs are not inferred when the history does not provide them.
325
+ - The labels are exactly `Name::`, `Author:`, `Source Code::`, `License::`,
326
+ `Contact::`, `Created::`, `Updated::` and `Copyright::`.
327
+ - `Created` is never changed.
328
+ - `Updated` is set to the date of a change that affects behaviour. A
329
+ documentation-only or formatting-only change does not touch it.
330
+ - The project-level copyright notice is `Copyright (c) 2012-2026 Automatic
331
+ Ruby Developers.` Explicit third-party copyright notices are preserved and
332
+ are not inferred from the `Author` field.
333
+ - The `License` line records the GPLv3 or LGPLv3 dual license and matches
334
+ [`LICENSE.md`](LICENSE.md).
335
+ - A `Description::` line may be added below `Name` when the file's purpose is
336
+ not obvious from its name. New plugins should have one.
337
+ - The magic encoding comment is redundant on the supported Rubies. It is left in
338
+ place in existing files, because removing it from forty files is a diff with
339
+ no benefit, and it is not required in a new file.
340
+
341
+ Files do not carry a per-file version history. This repository versions at the
342
+ repository level only; see section 10.
343
+
344
+ ### 2.4 Ruby version compatibility
345
+
346
+ - The supported range is stated in one place, `automatic.gemspec`
347
+ (`required_ruby_version`), and the README and the documents agree with it. The
348
+ set CI validates is a narrower statement and lives in the matrix; section 11
349
+ says how the two relate.
350
+ - **Compatibility is written in the range's common API.** Where a Ruby release
351
+ deprecates or removes something, the replacement chosen is the one that works
352
+ unchanged on every supported version. `URI::Parser#escape` becoming obsolete
353
+ is answered by naming `URI::RFC2396_Parser`, which means the same thing on all
354
+ of them — not by a `RUBY_VERSION` branch.
355
+ - **A `RUBY_VERSION` conditional is a last resort**, for a difference that has
356
+ no common expression. Two implementations of one behaviour cost more than the
357
+ compatibility they buy: the branch not taken is the branch not tested.
358
+ - **Code for an unsupported Ruby is removed, not kept for safety.** A
359
+ `RUBY_VERSION` comparison against 1.8 or 1.9, a branch for an interpreter that
360
+ cannot install the dependencies, and a shim for a method that has been in core
361
+ for a decade are all deleted.
362
+ - Nothing is written against a feature newer than the floor.
363
+ - A method removed by Ruby is replaced by its supported equivalent, and that is
364
+ a compatibility fix rather than a refactor: `Kernel#open` on a URL becomes
365
+ `URI.open`, `File.exists?` becomes `File.exist?`.
366
+ - **A library leaving the standard library is not by itself a reason to declare
367
+ it.** Ruby moves libraries to default and then to bundled gems as it goes.
368
+ Each one is judged on what actually requires it: the framework's own
369
+ requirement becomes a runtime dependency, a plugin's becomes an optional one
370
+ (section 9.1), and a requirement left over from code that no longer uses it is
371
+ deleted.
372
+
373
+ ### 2.5 Requiring
374
+
375
+ - The framework requires only what the framework uses. A plugin's library never
376
+ appears in `lib/`.
377
+ - A plugin requires its libraries at the top of its own file.
378
+ - A library needed only by an optional path is required inside that path, so the
379
+ plugin loads without it. The S3 branch of `StoreFile` is the example.
380
+ - The CLI requires a subcommand's libraries inside that subcommand, so a
381
+ command that does not use `feedbag` or the OPML parser does not load them.
382
+ - **An optional gem is required through `Automatic.require_optional`**, which
383
+ names the gem, what needed it and how to install it when it is absent. A bare
384
+ `require` of an optional gem answers a solvable problem with
385
+ `cannot load such file`, which is not the framework's best answer.
386
+ - **`Bundler.require` is not how the framework loads its dependencies.** An
387
+ installed library must not impose a bundle on the program requiring it. The
388
+ Bundler setup in `environment.rb` is a convenience for a source checkout and
389
+ does nothing when there is no `Gemfile`.
390
+
391
+ ---
392
+
393
+ ## 3. Plugins
394
+
395
+ - A new plugin follows [`PLUGINS.md`](PLUGINS.md) section 3, which is the
396
+ contract, and section 3.10, which says which category it belongs in.
397
+ - **Converting the pipeline into some other representation is a `Publish`
398
+ plugin's work, and the result stays inside the plugin.** It is serialized at
399
+ the boundary, written to the destination, and never passed along the pipeline
400
+ or made known to the framework, which keeps Invariant 2 and section 1.5
401
+ intact. Producing a portable document — one a person can read, ordinary tools
402
+ can process and another program can be given, with no service behind it — is
403
+ a destination like any other and belongs in that category.
404
+ - A new plugin is added to the catalogue in `PLUGINS.md` section 6 in the same
405
+ change, with its settings table and its status.
406
+ - A new plugin comes with a spec that reaches no network.
407
+ - A plugin's dependency is not added to the gemspec's runtime dependencies; see
408
+ section 9.
409
+ - A change to an existing plugin does not change what an existing Recipe means
410
+ unless that is the point of the change.
411
+
412
+ ---
413
+
414
+ ## 4. The life of a plugin
415
+
416
+ Plugins outlive the services they talk to. The policy for what happens then:
417
+
418
+ - **A shipped plugin has a current practical use.** That is the condition for
419
+ being in the gem, and it is a condition that has to keep being met, not one
420
+ met once. Having been useful is not the test.
421
+ - **A plugin is classified, in `PLUGINS.md` section 6**, as Supported, Supported
422
+ (external) or Needs rework, with the reason. There is no status meaning
423
+ "does not work and never will"; a plugin in that position is removed.
424
+ - **A dead integration is never faked into life.** No stub of a shut-down
425
+ service, no mock that makes an integration look alive, no test that asserts
426
+ against a simulation. This is Invariant 7 and it has no exceptions. If a
427
+ plugin can only be made to look supported by simulating what it talks to,
428
+ what it needs is deletion, not a double.
429
+ - **Unsupported code is not kept for preservation.** Git history holds every
430
+ implementation this project ever shipped, and holds it without installing it
431
+ on anyone's machine or listing it in a catalogue an operator reads for
432
+ guidance. A plugin retained only so that its code exists somewhere is
433
+ retained for a reason the version control system already covers.
434
+ - **The judgement is evidenced.** "Nobody uses that any more" is not a reason.
435
+ The service's own site, its API documentation or its published shutdown
436
+ notice is, and the reason goes in the catalogue entry or in the removal
437
+ record. Where the evidence cannot be obtained, the plugin is classified
438
+ **Needs rework** and kept: the failure mode of guessing is deleting something
439
+ that works.
440
+ - **Needs rework means restoration is realistic.** A capability the service
441
+ still offers, reachable by a current API, with the work amounting to a
442
+ migration rather than a new project. A plugin whose service is gone is not
443
+ Needs rework; it is removed.
444
+ - **Restoring a plugin to a service's current API** is a separate change, one
445
+ plugin at a time, with the catalogue entry updated in the same commit. Where
446
+ the current API forces a credential format the old Recipe cannot express,
447
+ that is a breaking change and is documented as one rather than hidden behind
448
+ a translation.
449
+ - **Removing a plugin removes all of it**: implementation, specs, example and
450
+ integration Recipes, catalogue entry, and any optional dependency nothing
451
+ else needs. A class left behind to raise "this no longer works" is not a
452
+ courtesy; the loader's `NoPluginError` says the same thing earlier and
453
+ without shipping code.
454
+ - **A removal is recorded in `VERSIONS`** in enough detail that an operator
455
+ whose Recipe breaks can find out why, and the reasons are kept in
456
+ `PLUGINS.md`. Removals are batched into a release rather than trickled, so
457
+ that an upgrade has one list to read.
458
+ - **A modernized plugin keeps its Recipe's meaning.** Class names, setting
459
+ names and defaults are not changed for tidiness; where a fix makes a plugin
460
+ behave as its documentation always said it did, that is still a behaviour
461
+ change and the catalogue entry says so.
462
+ - **A Supported plugin has deterministic local tests.** They cover this side of
463
+ the boundary — settings, request construction, serialization, response
464
+ handling, error behaviour — and reach no network, need no credential and
465
+ require no running service. The availability of somebody else's API is not
466
+ something a unit test can assert and is not something required CI waits on.
467
+
468
+ ---
469
+
470
+ ## 5. Testing
471
+
472
+ - **RSpec**, under `spec/`, mirroring the source tree: `spec/lib/` for the
473
+ framework and `spec/plugins/<category>/` for plugins.
474
+ - **The default suite reaches no network and needs no credential.** This is
475
+ Invariant 6. A spec that would is not written; the integration Recipes under
476
+ `test/integration/` are where that belongs, and they are run by hand.
477
+ - A framework spec covers the loader, the Recipe, the pipeline, the log and the
478
+ CLI's exit statuses.
479
+ - A plugin spec constructs the plugin with a `config` and a pipeline built by
480
+ `AutomaticSpec.generate_pipeline`, calls `run`, and asserts on the result.
481
+ - **An example that reaches a real host is tagged `:network`** and is excluded
482
+ from the default suite and from CI. It is kept, because it is a real test and
483
+ is the way to check a plugin against the service it talks to, and it is run
484
+ deliberately with `AUTOMATIC_NETWORK_SPECS=1`. Several of these point at hosts
485
+ that no longer serve what they expect, which is a further reason not to make
486
+ them a gate. A new example that reaches a host is tagged; one that does not is
487
+ never given the tag to make a failure go away.
488
+ - **The default suite does not depend on an optional plugin gem.** A gem the
489
+ `Gemfile` declares in an optional group is not installed by `bundle install`,
490
+ so the plugins that need it are not verified by the default suite or by the
491
+ required workflow. That is a decision, taken here, and not something a failure
492
+ discovers: the gems it applies to are the declared list
493
+ `AutomaticSpec::OPTIONAL_PLUGIN_GEMS`, a spec whose plugin needs one guards
494
+ its file with `AutomaticSpec.optional_dependency?`, and naming a gem that is
495
+ not on the list raises rather than skipping. Selecting the group with
496
+ `bundle config set --local with plugins` and installing runs those specs as
497
+ part of the ordinary suite.
498
+ - **A guard is a skip with a reason, not a `pending`.** A spec outside the
499
+ default suite prints why it is outside it and does not run; the default
500
+ suite's output is a list of what was verified rather than a list of what was
501
+ not.
502
+ - A spec whose plugin's gem is not installed is skipped by
503
+ `AutomaticSpec.plugin_available?`, which names the missing gem. That is the
504
+ intended behaviour and is not worked around by faking the gem.
505
+ - **The default suite is kept small and reliable rather than large.** It reaches
506
+ no network, needs no credential, needs no external daemon, writes outside no
507
+ temporary directory of its own, redirects `HOME`, and does not depend on
508
+ filesystem ordering, on the clock or on a random seed. A test that cannot be
509
+ made repeatable does not belong in it. Guaranteeing fewer things reliably is
510
+ the better trade, and it is not the same as weakening a test: `|| true`,
511
+ `continue-on-error` and a rescue that swallows a failure are forbidden, and
512
+ the tests that remain are held strictly.
513
+ - **A spec does not write outside its own temporary directory.** Where a plugin
514
+ resolves a path under the home directory, the spec redirects `HOME` to a
515
+ temporary directory rather than operating on the developer's real
516
+ `~/.automatic`.
517
+ - Coverage is measured with SimpleCov when `COVERAGE=on`, and is not a gate. The
518
+ historical `rcov` and `simplecov-rcov` tooling has been removed; it existed
519
+ for Ruby 1.8 and for a CI server that no longer runs.
520
+ - `rake spec` runs the suite; `rake spec:lib` and `rake spec:plugins` run the
521
+ halves. `rake` alone runs `spec`.
522
+ - CI runs `bundle install` and `rake spec` on every supported Ruby version, and
523
+ nothing that needs a secret.
524
+
525
+ ---
526
+
527
+ ## 6. Command line
528
+
529
+ - **The CLI is a library**, `Automatic::CLI`, and `bin/automatic` is a process
530
+ entry point that does nothing but call it and exit with its result.
531
+ - `CLI.run` returns an `Integer` and never calls `exit`, so the command line is
532
+ unit-testable.
533
+ - Exit statuses are fixed: `0` did the work or printed help or version, `1`
534
+ failed or nothing was asked for, `2` the command line was rejected.
535
+ - `--help` and `--version` exit `0` and print to standard output.
536
+ - Option parsing uses `optparse` from the standard library. **No CLI framework
537
+ is introduced**; the interface is one option and seven subcommands, and a gem
538
+ for that would be a dependency for nothing.
539
+ - The subcommands and the meaning of `-c` are part of the compatibility promise.
540
+ Adding a subcommand is fine; removing or renaming one is a breaking change.
541
+ - A subcommand prints its result to standard output, because printing is what it
542
+ is for.
543
+
544
+ ---
545
+
546
+ ## 7. Backward compatibility
547
+
548
+ What this repository promises not to break, absent a deliberate and recorded
549
+ decision:
550
+
551
+ | Interface | Promise |
552
+ | --- | --- |
553
+ | Recipe format | A Recipe that worked keeps working |
554
+ | Plugin contract | `new(config, pipeline)` and `run` |
555
+ | Plugin naming | The class-name-to-file-path rule |
556
+ | User directory | `~/.automatic` and its four subdirectories |
557
+ | User plugin precedence | The user directory shadows the installation |
558
+ | CLI | `-c`, the subcommand names, and the exit statuses |
559
+ | Store databases | Existing SQLite files stay readable |
560
+ | Gem name | `automatic`, providing the `automatic` executable |
561
+
562
+ Breaking one of these is a decision, taken deliberately, recorded in
563
+ [`VERSIONS`](VERSIONS) in terms an operator can act on, and never a side effect
564
+ of a clean-up. Adding an optional setting whose default preserves current
565
+ behaviour breaks nothing.
566
+
567
+ ---
568
+
569
+ ## 8. Documentation
570
+
571
+ - **Documentation is updated in the same change as the behaviour it describes.**
572
+ A behaviour change with no documentation change has not been finished.
573
+ - The documents divide as follows, and one fact has one home:
574
+
575
+ | Document | Holds |
576
+ | --- | --- |
577
+ | `README.md` | The entry point: what it is, how to install and run it, where everything else is |
578
+ | `doc/REQUIREMENTS.md` | What the system is for and what it guarantees |
579
+ | `doc/BASIC_DESIGN.md` | How it is composed |
580
+ | `doc/PLUGINS.md` | The Recipe format, the plugin contract, the plugin catalogue |
581
+ | `doc/POLICY.md` | This document: how a change is made and judged |
582
+ | `doc/DEPLOYMENT.md` | Installing, running and operating it |
583
+ | `doc/VERSIONS` | Release history |
584
+ | `doc/LICENSE.md`, `doc/COPYING`, `doc/COPYING.LESSER` | The licence |
585
+ | `doc/AUTHORS` | Contributors |
586
+
587
+ - **No cross-repository reference.** These documents never say "see the policy
588
+ of another repository", or "as in *X*". Everything needed to understand,
589
+ build, run and change this repository is here. Another repository may be a
590
+ useful reference while working, and it is not a citation.
591
+ - **Documents are in English**, as are code, comments and commit messages.
592
+ Japanese appears only where it is data: a search keyword in an example, a
593
+ broadcast station name in a plugin's settings.
594
+ - Markdown documents may assume a renderer. `VERSIONS`, `COPYING`, `COPYING.LESSER` and `AUTHORS`
595
+ are plain text and keep their extensionless names, which are the names they
596
+ are published and linked under.
597
+ - Prose wraps near the width the document already uses. `VERSIONS` follows
598
+ section 10.4 instead.
599
+ - **A document is not deleted for being old.** It is deleted when its content
600
+ has been moved somewhere that is now the source of truth, and the move is
601
+ recorded in `VERSIONS`.
602
+
603
+ ---
604
+
605
+ ## 9. Dependencies
606
+
607
+ ### 9.1 The split
608
+
609
+ Install the core by default; install a plugin's dependencies only when they are
610
+ needed. Dependencies fall into three groups, and which group a gem is in is a
611
+ decision, not an accident:
612
+
613
+ **Runtime dependencies** — declared in `automatic.gemspec`, installed by `gem
614
+ install automatic`. A gem is here only if a file in `lib/` requires it: what
615
+ `require 'automatic'`, loading a Recipe, loading a plugin, running a pipeline
616
+ and the command line itself need, and nothing more. A gem that reached this list
617
+ because of a plugin, because a plugin no longer uses it, or because a Ruby
618
+ release moved a library out of the standard library, is moved back out.
619
+
620
+ **Optional dependencies** — used by one plugin or a few, required inside the
621
+ plugin's own file, declared in an optional group of the `Gemfile`, and **not
622
+ declared as runtime dependencies**. An operator who uses that plugin installs
623
+ the gem. This is Invariant 4, and it is why installing this gem does not install
624
+ an AWS SDK.
625
+
626
+ The permanent rules of the split:
627
+
628
+ - **A core dependency is one the framework itself has.** A plugin's dependency
629
+ is never a framework runtime dependency, however useful, popular or
630
+ Supported that plugin is. How many Recipes happen to use it is not the test;
631
+ what `lib/` requires is.
632
+ - **The operator who uses the plugin installs its gem.** Not having it must
633
+ never stop the framework from starting, and a Recipe that does not name the
634
+ plugin must run without it.
635
+ - **A new plugin does not increase the core dependencies.** Adding one to the
636
+ runtime list needs an architectural justification — the framework itself
637
+ came to need the gem — recorded with the change; wanting the plugin to work
638
+ out of the box is not one.
639
+ - **The default tests and required CI do not depend on an optional
640
+ integration.** Being in this group has that intended consequence: those
641
+ plugins are not part of what a green build guarantees. See section 5.
642
+ - **An unsupported or optional integration does not decide a framework-wide
643
+ dependency.** Where one plugin needs a gem, that gem is the plugin's.
644
+
645
+ A missing optional gem is reported, not merely raised: the plugin requires it
646
+ through `Automatic.require_optional`, which names the gem, what needed it and
647
+ how to install it. That helper is the whole of the mechanism, and no dependency
648
+ manager, plugin manifest or resolver is introduced beyond it.
649
+
650
+ **Development dependencies** — the test and build tooling.
651
+
652
+ ### 9.2 Adding, updating and removing
653
+
654
+ - A gem is added when something committed here uses it, and the commit says
655
+ which plugin and why.
656
+ - **Dependencies are not raised in bulk.** Each is updated for a reason —
657
+ a security fix, a Ruby compatibility requirement, an API this repository
658
+ needs — and the code that uses it is checked against the new version. Bumping
659
+ everything to latest and then repairing what broke is not how this is done.
660
+ - Version constraints are ranges wide enough not to conflict with the rest of an
661
+ operator's bundle, and tight enough to exclude a major version this code has
662
+ not been checked against.
663
+ - A gem no longer used by anything committed here is removed.
664
+ - A gem whose service no longer exists is removed, along with the plugin that
665
+ needed it; see section 4. An optional group left with nothing to install is
666
+ deleted from the `Gemfile` in the same change.
667
+ - **Nothing is vendored.** Dependencies come from RubyGems, which keeps their
668
+ licences theirs.
669
+
670
+ ### 9.3 Sources
671
+
672
+ - One gem source: `https://rubygems.org`.
673
+ - **No plain-HTTP source, ever.** `http://rubygems.org` in a `Gemfile` is a
674
+ defect, not a style question.
675
+ - No source that has shut down. `gems.github.com` has not existed since 2014 and
676
+ is not to reappear.
677
+
678
+ ### 9.4 Packaging
679
+
680
+ - The gemspec is **hand-maintained**. It was generated by Jeweler, which is no
681
+ longer maintained and which required its own Rake tasks and a `VERSION` file
682
+ to regenerate a file that is easier to simply edit. The generator is gone; the
683
+ file is now source.
684
+ - The gemspec states `required_ruby_version`, the licence, the homepage, the
685
+ source code URL and the metadata links, and its `files` list is derived from
686
+ what Git tracks so that it cannot drift.
687
+ - The `Gemfile` declares the source and evaluates the gemspec, so that runtime
688
+ dependencies are stated once. Development and optional gems are groups in the
689
+ `Gemfile`.
690
+ - `Gemfile.lock` is not committed. This is a library, and locking would impose a
691
+ resolution on every consumer.
692
+ - `Rakefile` carries the test tasks and nothing else. It is not a build system,
693
+ and no build system is introduced.
694
+ - A release requires green required CI. Its built gem is inspected and installed
695
+ locally before publication, and its source version and metadata must agree.
696
+ - A published version is immutable. Credentials are never committed, and
697
+ release commits and tags are not rewritten to conceal a publication error.
698
+ - The manual publication procedure is [`RELEASING.md`](RELEASING.md). Publishing
699
+ is an explicit maintainer action, not a side effect of a build or test task.
700
+
701
+ ---
702
+
703
+ ## 10. Versioning
704
+
705
+ ### 10.1 Scheme
706
+
707
+ Releases are numbered `<year>.<month>`, two digits each, taken from the release
708
+ date. This is the scheme the project has used since its first release in
709
+ February 2012, it mimics Ubuntu's, and it does not change.
710
+
711
+ ```text
712
+ 26.08 a release made in August 2026
713
+ 26.08.1 a release correcting 26.08, in the same month
714
+ ```
715
+
716
+ A third `<patch>` level is appended only when a release corrects an earlier one
717
+ without accumulating a month's work. `14.12.1` and `14.12.2` are historical
718
+ examples.
719
+
720
+ The number carries no compatibility meaning. A month is not a major version, and
721
+ a compatibility break is signalled by what the `VERSIONS` entry says, not by the
722
+ number.
723
+
724
+ ### 10.2 Where the version is written
725
+
726
+ The release version appears in three places, which are changed together in one
727
+ commit:
728
+
729
+ | Place | Form |
730
+ | --- | --- |
731
+ | `VERSION` | `26.08` |
732
+ | `lib/automatic/version.rb` | `Automatic::VERSION` |
733
+ | `doc/VERSIONS` | The entry heading |
734
+
735
+ `automatic.gemspec` reads `VERSION`, and `automatic --version` prints
736
+ `Automatic::VERSION`, so neither is a separate place to update.
737
+
738
+ Historically `lib/automatic/version.rb` carried a `-devel` suffix between
739
+ releases while `VERSION` did not, and a spec asserted the exact string. That
740
+ divergence is not continued: the two agree, and the spec asserts that they
741
+ agree rather than asserting a literal.
742
+
743
+ ### 10.3 What is a release
744
+
745
+ - **Work that is not released yet takes no version of its own.** It belongs to
746
+ the entry already standing at the top of `doc/VERSIONS`.
747
+ - An unreleased entry carries `(Release Date: TBD)`. Replacing that with the
748
+ date is the release itself, and is not a change to record inside the entry.
749
+ - **A repository that has not yet made its first release is in its initial
750
+ construction stage**, and that stage takes no entry here. The work of building
751
+ up to the first release is not accumulated in `doc/VERSIONS` one by one: the
752
+ file is the record of released versions, not of the construction that precedes
753
+ the first of them, and its first entry is written when that release is made.
754
+ - **A series of changes made on one day is one version**, not several. Do not
755
+ split a day's work across version numbers.
756
+ - **A version that actually existed is never merged into another**, even when it
757
+ shares a date with one. `14.10.0` and `14.10.1` were both released on
758
+ 2014-10-23 and both stay.
759
+ - A documentation-only change takes no entry unless its scale makes it worth one
760
+ line saying so.
761
+ - Git tags carry the release version, and are created only when a release is
762
+ made.
763
+
764
+ ### 10.4 How `doc/VERSIONS` is written
765
+
766
+ `doc/VERSIONS` is a version-level summary of what each release changed. It is
767
+ not a transcription of the commit log: the commits record how the work happened,
768
+ the version history records what it amounts to.
769
+
770
+ - Record externally meaningful or architecturally significant outcomes, such
771
+ as compatibility, security, public interfaces, major features, packaging,
772
+ licensing, documentation architecture and test strategy.
773
+ - Omit development-time corrections, implementation details and intermediate
774
+ states that were reverted or superseded before release. Git history retains
775
+ that process.
776
+ - Summarize related low-level changes under their substantive outcome instead
777
+ of listing each method, dependency, test or file separately.
778
+
779
+ - Each entry opens with `vX.YY (YYYY-MM-DD)`, or `vX.YY (Release Date: TBD)`
780
+ while unreleased, underlined with `-`, followed by one `-` bullet per change.
781
+ Newest first. UTF-8.
782
+ - **One coherent change is one bullet on one physical line.** The entry is a
783
+ list meant to be scanned, and a wrapped bullet costs it that: the eye no
784
+ longer finds the changes by counting lines, and a diff no longer shows one
785
+ added line per added change.
786
+ - This is a deliberate exception to the wrapping the other plain text documents
787
+ follow. Do not rewrap `doc/VERSIONS` to 80 columns, and do not report a long
788
+ bullet there as a defect.
789
+ - Aim for about 100 columns. A bullet carrying file names, module names, setting
790
+ names or plugin names may run to about 120, or past it when the names it needs
791
+ are that long. These figures prompt a reread, they are not a limit to enforce.
792
+ A bullet that is long because the change is long is correct.
793
+ - **When a bullet runs long, abstract it; never break it across lines.** Drop
794
+ the implementation detail, the example, the reason and the secondary effect,
795
+ and state what the change is. Keep what a reader cannot reconstruct without
796
+ it: what changed, what is now observably different, what it does to
797
+ compatibility, what it does to security, and the identifiers someone would
798
+ search for.
799
+ - Changes serving one purpose are described together even when they touch
800
+ several files. Related changes to one file within a version are normally one
801
+ bullet. Changes to one file carrying independent meaning are not forced
802
+ together — coherence decides, not the file name.
803
+ - Entries touching the same file, plugin or feature are placed near each other,
804
+ so that a version reads as a coherent whole. An independent change belonging
805
+ with nothing already listed is appended to the end of the current entry.
806
+ - Order within a version serves the reader, not the commit history.
807
+ - Released entries retain their substantive history even when their wording or
808
+ level of detail predates these rules.
809
+ - `doc/VERSIONS` carries these guidelines again at its foot.
810
+
811
+ ### 10.5 The historical record
812
+
813
+ - **Past entries and their dates are not rewritten**, not to correct their
814
+ wording, not to renumber them, and not to make them consistent with this
815
+ document's current rules. They are the record of what was released.
816
+ - The release history was previously kept in `doc/ChangeLog`. It has been moved
817
+ into `doc/VERSIONS` in full, and `doc/ChangeLog` is gone; the two documents
818
+ had the same responsibility and keeping both meant keeping two records that
819
+ would diverge. No entry and no date was changed in the move.
820
+ - Where the record is incomplete — releases with no tag, or a version bumped
821
+ with no entry — `VERSIONS` says so rather than inventing an entry.
822
+
823
+ ---
824
+
825
+ ## 11. Continuous integration
826
+
827
+ - CI runs on GitHub Actions. `.github/workflows/ci.yml` is the required check;
828
+ `.github/workflows/plugins.yml` is a separate, non-required workflow that
829
+ installs the optional `plugins` group and runs the same suite, so that the
830
+ documented all-plugins setup is checked as well as described.
831
+ - **CI validates representative supported Ruby versions rather than every
832
+ intermediate release.** The matrix runs the ends of the supported range and
833
+ the release in the middle. The matrix and `required_ruby_version` are
834
+ therefore *not* the same set, and neither is wrong: the gemspec states what
835
+ the code is written for, the matrix states what is checked on every commit.
836
+ [`REQUIREMENTS.md`](REQUIREMENTS.md) section 20 states both.
837
+ - Removing a version from the matrix is not a statement that it fails, and no
838
+ incompatibility is introduced to make it one.
839
+ - What CI does is: install the bundle, build the gem, load the library, run the
840
+ command line, run the default suite. It is deliberately short, and an optional
841
+ integration is not added to it.
842
+ - **CI holds no secret and reaches no external service.** No credential is
843
+ configured, and no integration test against a third-party API is run there.
844
+ - **The required workflow installs no optional plugin gem**, so no plugin's own
845
+ dependency is a condition of the check that gates a change. The minimal
846
+ configuration is what it runs, which is what keeps the split of section 9.1
847
+ honest over time. Where an optional integration is worth testing at all, it is
848
+ tested separately from the required workflow; section 5 says how.
849
+ - **A non-required workflow is held to the same standard as the required one.**
850
+ It is separate so that an optional dependency cannot gate a change, not so
851
+ that it may fail quietly.
852
+ - **A failure is fixed, not silenced.** `|| true`, `continue-on-error` and a
853
+ step that hides its exit status are not how a build is made green. Narrowing
854
+ what is guaranteed is a legitimate answer; pretending to guarantee it is not.
855
+ - The Jenkins instance the project used until 2015 is gone. References to it
856
+ have been removed and are not to be reintroduced.
857
+ - A red build is fixed or reverted. It is not left red.