automatic 14.12.2 → 26.08
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +5 -5
- data/README.md +635 -83
- data/VERSION +1 -1
- data/automatic.gemspec +109 -248
- data/bin/automatic +20 -139
- data/config/feed2console.yml +10 -4
- data/config/feed2markdown.yml +41 -0
- data/doc/AI_TUTORIAL.md +518 -0
- data/doc/BASIC_DESIGN.md +516 -0
- data/doc/COPYING.LESSER +165 -0
- data/doc/DEPLOYMENT.md +824 -0
- data/doc/LICENSE.md +14 -0
- data/doc/PLUGINS.md +1875 -0
- data/doc/PLUGIN_DEVELOPMENT.md +86 -0
- data/doc/POLICY.md +857 -0
- data/doc/QUICKSTART.md +256 -0
- data/doc/RELEASING.md +381 -0
- data/doc/REQUIREMENTS.md +526 -0
- data/doc/VERSIONS +208 -0
- data/lib/automatic/cli.rb +248 -0
- data/lib/automatic/environment.rb +31 -5
- data/lib/automatic/feed_maker.rb +10 -9
- data/lib/automatic/feed_parser.rb +51 -35
- data/lib/automatic/http.rb +107 -0
- data/lib/automatic/log.rb +49 -18
- data/lib/automatic/opml.rb +3 -1
- data/lib/automatic/pipeline.rb +63 -32
- data/lib/automatic/recipe.rb +56 -17
- data/lib/automatic/version.rb +14 -1
- data/lib/automatic.rb +78 -20
- data/plugins/custom_feed/svn_log.rb +73 -32
- data/plugins/custom_feed/web.rb +348 -0
- data/plugins/filter/absolute_uri.rb +43 -27
- data/plugins/filter/accept.rb +38 -45
- data/plugins/filter/claude.rb +217 -0
- data/plugins/filter/clear.rb +12 -8
- data/plugins/filter/description_link.rb +49 -51
- data/plugins/filter/full_feed.rb +158 -52
- data/plugins/filter/gemini.rb +216 -0
- data/plugins/filter/github_feed.rb +38 -26
- data/plugins/filter/ignore.rb +33 -44
- data/plugins/filter/image.rb +36 -25
- data/plugins/filter/image_source.rb +58 -52
- data/plugins/filter/join.rb +107 -0
- data/plugins/filter/one.rb +19 -26
- data/plugins/filter/open_ai.rb +198 -0
- data/plugins/filter/rand.rb +16 -17
- data/plugins/filter/sakura_ai.rb +205 -0
- data/plugins/filter/sanitize.rb +29 -34
- data/plugins/filter/sort.rb +20 -27
- data/plugins/filter/tumblr_resize.rb +31 -23
- data/plugins/notify/ikachan.rb +86 -48
- data/plugins/provide/fluentd.rb +43 -24
- data/plugins/publish/amazon_s3.rb +73 -40
- data/plugins/publish/console.rb +19 -16
- data/plugins/publish/console_link.rb +20 -17
- data/plugins/publish/eject.rb +48 -26
- data/plugins/publish/fluentd.rb +50 -30
- data/plugins/publish/hatena_bookmark.rb +88 -71
- data/plugins/publish/instapaper.rb +69 -59
- data/plugins/publish/markdown.rb +278 -0
- data/plugins/publish/memcached.rb +35 -29
- data/plugins/store/database.rb +50 -48
- data/plugins/store/digest.rb +212 -0
- data/plugins/store/file.rb +99 -68
- data/plugins/store/full_text.rb +32 -25
- data/plugins/store/permalink.rb +18 -22
- data/plugins/subscription/feed.rb +34 -21
- data/plugins/subscription/link.rb +31 -32
- data/plugins/subscription/text.rb +32 -46
- data/plugins/subscription/tumblr.rb +55 -44
- data/plugins/subscription/xml.rb +40 -36
- metadata +108 -321
- data/Gemfile +0 -38
- data/Rakefile +0 -59
- data/doc/ChangeLog +0 -303
- data/doc/PLUGINS +0 -750
- data/doc/PLUGINS.ja +0 -753
- data/doc/README +0 -511
- data/doc/README.ja +0 -519
- data/plugins/filter/google_news.rb +0 -50
- data/plugins/publish/google_calendar.rb +0 -84
- data/plugins/publish/hipchat.rb +0 -46
- data/plugins/publish/pocket.rb +0 -45
- data/plugins/publish/twitter.rb +0 -58
- data/plugins/subscription/chan_toru.rb +0 -57
- data/plugins/subscription/g_guide.rb +0 -57
- data/plugins/subscription/pocket.rb +0 -51
- data/plugins/subscription/twitter.rb +0 -69
- data/plugins/subscription/twitter_search.rb +0 -50
- data/plugins/subscription/weather.rb +0 -33
- data/script/build +0 -84
- data/spec/fixtures/sampleFeeds.tsv +0 -1
- data/spec/fixtures/sampleFeeds2.tsv +0 -2
- data/spec/fixtures/sampleRecipe.yml +0 -24
- data/spec/lib/automatic/log_spec.rb +0 -32
- data/spec/lib/automatic/pipeline_spec.rb +0 -68
- data/spec/lib/automatic/recipe_spec.rb +0 -40
- data/spec/lib/automatic_spec.rb +0 -99
- data/spec/plugins/custom_feed/svn_log_spec.rb +0 -31
- data/spec/plugins/filter/absolute_uri_spec.rb +0 -61
- data/spec/plugins/filter/accept_spec.rb +0 -331
- data/spec/plugins/filter/clear_spec.rb +0 -49
- data/spec/plugins/filter/description_link_spec.rb +0 -138
- data/spec/plugins/filter/full_feed_spec.rb +0 -129
- data/spec/plugins/filter/github_feed_spec.rb +0 -55
- data/spec/plugins/filter/google_news_spec.rb +0 -69
- data/spec/plugins/filter/ignore_spec.rb +0 -328
- data/spec/plugins/filter/image_source_spec.rb +0 -89
- data/spec/plugins/filter/image_spec.rb +0 -65
- data/spec/plugins/filter/one_spec.rb +0 -71
- data/spec/plugins/filter/rand_spec.rb +0 -52
- data/spec/plugins/filter/sanitize_spec.rb +0 -153
- data/spec/plugins/filter/sort_spec.rb +0 -189
- data/spec/plugins/filter/tumblr_resize_spec.rb +0 -109
- data/spec/plugins/notify/ikachan_spec.rb +0 -58
- data/spec/plugins/provide/fluentd_spec.rb +0 -49
- data/spec/plugins/publish/amazon_s3_spec.rb +0 -40
- data/spec/plugins/publish/console_spec.rb +0 -30
- data/spec/plugins/publish/eject_spec.rb +0 -40
- data/spec/plugins/publish/fluentd_spec.rb +0 -40
- data/spec/plugins/publish/google_calendar_spec.rb +0 -83
- data/spec/plugins/publish/hatena_bookmark_spec.rb +0 -134
- data/spec/plugins/publish/hipchat_spec.rb +0 -69
- data/spec/plugins/publish/instapaper_spec.rb +0 -82
- data/spec/plugins/publish/memcached_spec.rb +0 -63
- data/spec/plugins/publish/pocket_spec.rb +0 -51
- data/spec/plugins/publish/twitter_spec.rb +0 -73
- data/spec/plugins/store/file_spec.rb +0 -58
- data/spec/plugins/store/full_text_spec.rb +0 -152
- data/spec/plugins/store/permalink_spec.rb +0 -206
- data/spec/plugins/subscription/chan_toru_spec.rb +0 -56
- data/spec/plugins/subscription/feed_spec.rb +0 -71
- data/spec/plugins/subscription/g_guide_spec.rb +0 -82
- data/spec/plugins/subscription/link_spec.rb +0 -72
- data/spec/plugins/subscription/pocket_spec.rb +0 -57
- data/spec/plugins/subscription/text_spec.rb +0 -84
- data/spec/plugins/subscription/tumblr_spec.rb +0 -74
- data/spec/plugins/subscription/twitter_search_spec.rb +0 -57
- data/spec/plugins/subscription/twitter_spec.rb +0 -73
- data/spec/plugins/subscription/weather_spec.rb +0 -44
- data/spec/plugins/subscription/xml_spec.rb +0 -84
- data/spec/spec_helper.rb +0 -106
- data/spec/user_dir/plugins/store/mock.rb +0 -16
- data/test/fixtures/sampleOPML.xml +0 -11
- data/test/integration/test_absoluteurl.yml +0 -25
- data/test/integration/test_activerecord.yml +0 -24
- data/test/integration/test_add_pocket.yml +0 -26
- data/test/integration/test_chan_toru.yml +0 -21
- data/test/integration/test_descriptionlink.yml +0 -21
- data/test/integration/test_fluentd.yml +0 -22
- data/test/integration/test_fulltext.yml +0 -30
- data/test/integration/test_google_news.yml +0 -21
- data/test/integration/test_googlealert.yml +0 -21
- data/test/integration/test_hatenabookmark.yml +0 -30
- data/test/integration/test_ignore.yml +0 -25
- data/test/integration/test_ignore2.yml +0 -22
- data/test/integration/test_image2local.yml +0 -33
- data/test/integration/test_instapaper.yml +0 -26
- data/test/integration/test_link2local.yml +0 -34
- data/test/integration/test_one.yml +0 -23
- data/test/integration/test_pocket.yml +0 -22
- data/test/integration/test_rand.yml +0 -21
- data/test/integration/test_sanitize.yml +0 -23
- data/test/integration/test_sort.yml +0 -36
- data/test/integration/test_svnlog.yml +0 -15
- data/test/integration/test_text2feed.yml +0 -36
- data/test/integration/test_tumblr2local.yml +0 -43
- data/test/integration/test_twitter_search.yml +0 -22
- data/test/integration/test_weather.yml +0 -19
- data/test/integration/test_xml2fluentd.yml +0 -21
- data/vendor/.gitkeep +0 -0
data/doc/QUICKSTART.md
ADDED
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
# Quick Start
|
|
2
|
+
|
|
3
|
+
This guide takes four public pages through one short Automatic Ruby pipeline and
|
|
4
|
+
leaves what they publish as one Markdown document. It needs no account, no
|
|
5
|
+
credential, no paid service and no database server.
|
|
6
|
+
|
|
7
|
+
It does need two of the optional gems — because of the plugins the Recipe
|
|
8
|
+
names, not because of the framework — and installing exactly those is a step of
|
|
9
|
+
this guide rather than a footnote to it. Automatic Ruby installs what the
|
|
10
|
+
framework needs and leaves a plugin's gems to the operator who uses that
|
|
11
|
+
plugin, so "which plugins does this Recipe name, and what do they need" is a
|
|
12
|
+
question every Recipe asks. Skipping it is the usual way a first run stops half
|
|
13
|
+
way through.
|
|
14
|
+
|
|
15
|
+
## 1. Install
|
|
16
|
+
|
|
17
|
+
Use Ruby 3.3 through 4.0 on a Unix-like system:
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
ruby -v
|
|
21
|
+
gem install automatic
|
|
22
|
+
automatic --version
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Working from a Git checkout instead is this same guide with one step done
|
|
26
|
+
differently; "From a source checkout" at the end is that step.
|
|
27
|
+
|
|
28
|
+
## 2. Create the user directory
|
|
29
|
+
|
|
30
|
+
```sh
|
|
31
|
+
automatic scaffold
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
This creates `~/.automatic` and copies the shipped examples to
|
|
35
|
+
`~/.automatic/config/example`. Existing files and directories are not
|
|
36
|
+
overwritten.
|
|
37
|
+
|
|
38
|
+
## 3. Write the Recipe
|
|
39
|
+
|
|
40
|
+
A Recipe is one job: the plugins it runs, in order, with their settings. This
|
|
41
|
+
one reads four public index pages as HTML and makes a feed of the articles each
|
|
42
|
+
lists — which is what to do for a page whose feed you do not have — keeps a
|
|
43
|
+
record of what it has already seen, and appends the rest to a Markdown
|
|
44
|
+
document.
|
|
45
|
+
|
|
46
|
+
Write it to `~/.automatic/config/web2markdown.yml`:
|
|
47
|
+
|
|
48
|
+
```yaml
|
|
49
|
+
plugins:
|
|
50
|
+
- module: CustomFeedWeb
|
|
51
|
+
config:
|
|
52
|
+
retry: 2
|
|
53
|
+
interval: 2
|
|
54
|
+
sites:
|
|
55
|
+
# Articles: https://blog.python.org/2026/08/python-3147-31315/
|
|
56
|
+
- name: Python Insider
|
|
57
|
+
url: https://blog.python.org/
|
|
58
|
+
link_selector: 'a[href]'
|
|
59
|
+
include:
|
|
60
|
+
- '^https://blog\.python\.org/20[0-9]{2}/[0-9]{2}/[^/]+/?$'
|
|
61
|
+
same_host: true
|
|
62
|
+
fetch_items: 20
|
|
63
|
+
|
|
64
|
+
# Articles: https://blog.rust-lang.org/2026/08/04/enabling-polonius-alpha-on-nightly/
|
|
65
|
+
- name: Rust Blog
|
|
66
|
+
url: https://blog.rust-lang.org/
|
|
67
|
+
link_selector: 'a[href]'
|
|
68
|
+
include:
|
|
69
|
+
- '^https://blog\.rust-lang\.org/20[0-9]{2}/[0-9]{2}/[0-9]{2}/[^/]+/?$'
|
|
70
|
+
same_host: true
|
|
71
|
+
fetch_items: 20
|
|
72
|
+
|
|
73
|
+
# Articles: https://go.dev/blog/pkgsite-api
|
|
74
|
+
- name: The Go Blog
|
|
75
|
+
url: https://go.dev/blog/
|
|
76
|
+
link_selector: 'a[href]'
|
|
77
|
+
include:
|
|
78
|
+
- '^https://go\.dev/blog/[^/]+$'
|
|
79
|
+
same_host: true
|
|
80
|
+
fetch_items: 20
|
|
81
|
+
|
|
82
|
+
# Alerts: https://www.jpcert.or.jp/at/2026/at260021.html
|
|
83
|
+
- name: JPCERT/CC Alerts
|
|
84
|
+
url: https://www.jpcert.or.jp/at/2026.html
|
|
85
|
+
link_selector: 'a[href]'
|
|
86
|
+
include:
|
|
87
|
+
- '^https://www\.jpcert\.or\.jp/at/20[0-9]{2}/at[0-9]+\.html$'
|
|
88
|
+
same_host: true
|
|
89
|
+
fetch_items: 20
|
|
90
|
+
|
|
91
|
+
- module: StoreDigest
|
|
92
|
+
config:
|
|
93
|
+
db: web2markdown.db
|
|
94
|
+
fields:
|
|
95
|
+
- title
|
|
96
|
+
- link
|
|
97
|
+
|
|
98
|
+
- module: PublishMarkdown
|
|
99
|
+
config:
|
|
100
|
+
file: ~/.automatic/markdown/web.md
|
|
101
|
+
mode: append
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Three plugins, and each hands its result to the next:
|
|
105
|
+
|
|
106
|
+
- **`CustomFeedWeb`** fetches each page and makes a feed of the article links it
|
|
107
|
+
lists. `include` is what tells an article from a navigation link, `interval`
|
|
108
|
+
is the pause between requests, and one run makes one request per site —
|
|
109
|
+
nothing here follows a link or reads an article body.
|
|
110
|
+
- **`StoreDigest`** records a digest of each item and passes on only the items
|
|
111
|
+
whose digest it had not recorded already. It is what makes the Recipe safe to
|
|
112
|
+
run repeatedly. `db` is a **file name**, kept under `~/.automatic/db`.
|
|
113
|
+
- **`PublishMarkdown`** appends what is left to a plain-text document, creating
|
|
114
|
+
the directory if it is missing. Its `file`, unlike `db`, is a path.
|
|
115
|
+
|
|
116
|
+
## 4. Install what the Recipe needs
|
|
117
|
+
|
|
118
|
+
Read the Recipe you have just written, plugin by plugin, and look each one up in
|
|
119
|
+
the table of optional plugin dependencies in [`DEPLOYMENT.md`](DEPLOYMENT.md).
|
|
120
|
+
That table gives, for these three:
|
|
121
|
+
|
|
122
|
+
- **`CustomFeedWeb`** — `nokogiri`, which it reads the pages with. In a
|
|
123
|
+
checkout, the group `html`.
|
|
124
|
+
- **`StoreDigest`** — `activerecord` and `sqlite3`, which it keeps its record
|
|
125
|
+
in. In a checkout, the group `store`.
|
|
126
|
+
- **`PublishMarkdown`** — nothing of its own.
|
|
127
|
+
|
|
128
|
+
```sh
|
|
129
|
+
gem install nokogiri
|
|
130
|
+
gem install activerecord sqlite3
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
**Install what the whole Recipe needs, not what its first plugin needs.** A
|
|
134
|
+
plugin is loaded when the pipeline reaches it, so installing only `nokogiri`
|
|
135
|
+
would let `CustomFeedWeb` fetch its pages, hand its feeds on, and stop the run
|
|
136
|
+
where the next plugin is loaded:
|
|
137
|
+
|
|
138
|
+
```text
|
|
139
|
+
automatic: The `activerecord` gem is not installed. It is needed by the store
|
|
140
|
+
plugins. Install it with `gem install activerecord`, or in a source checkout add
|
|
141
|
+
its group to the bundle; see the optional plugin dependencies in
|
|
142
|
+
doc/DEPLOYMENT.md. (cannot load such file -- active_record)
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
`PublishMarkdown` is the entry worth reading twice: it needs no gem of its own,
|
|
146
|
+
using an HTML parser where one is installed and its own substitution where none
|
|
147
|
+
is. A plugin's row in that table is the answer, not a guess from what the
|
|
148
|
+
plugin does.
|
|
149
|
+
|
|
150
|
+
`nokogiri` and `sqlite3` build a native extension where no binary package
|
|
151
|
+
matches your platform. Install a build environment only if one of them says it
|
|
152
|
+
needs one.
|
|
153
|
+
|
|
154
|
+
## 5. Run it
|
|
155
|
+
|
|
156
|
+
```sh
|
|
157
|
+
automatic -c ~/.automatic/config/web2markdown.yml
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
A bare name is resolved inside `~/.automatic/config`, so `automatic -c
|
|
161
|
+
web2markdown.yml` is the same command. The log names each page as it is fetched,
|
|
162
|
+
each digest as it is saved, and the document as it is written.
|
|
163
|
+
|
|
164
|
+
## 6. Read the result
|
|
165
|
+
|
|
166
|
+
```sh
|
|
167
|
+
sed -n '1,80p' ~/.automatic/markdown/web.md
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Each item is a level-2 heading followed by the metadata the feed carries:
|
|
171
|
+
|
|
172
|
+
```markdown
|
|
173
|
+
## Extending the pkgsite API
|
|
174
|
+
|
|
175
|
+
- Link: <https://go.dev/blog/pkgsite-api>
|
|
176
|
+
- Date: 2026-08-17 09:00:00 +0900
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## 7. Run it again
|
|
180
|
+
|
|
181
|
+
```sh
|
|
182
|
+
automatic -c ~/.automatic/config/web2markdown.yml
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
The second run appends nothing: the pages still list the same articles, and
|
|
186
|
+
`StoreDigest` has the digest of every one of them. An article published between
|
|
187
|
+
the two runs is the one thing that would be added — which is what makes this
|
|
188
|
+
Recipe safe to put in `cron`, and what to check before any Recipe with an
|
|
189
|
+
effect.
|
|
190
|
+
|
|
191
|
+
If instead everything is appended a second time, the store plugin is writing
|
|
192
|
+
somewhere other than where you think. The `Using Database:` line of the log
|
|
193
|
+
names the file it opened.
|
|
194
|
+
|
|
195
|
+
## 8. Run it from cron
|
|
196
|
+
|
|
197
|
+
Create the log directory once, then use the absolute path reported by
|
|
198
|
+
`command -v automatic`:
|
|
199
|
+
|
|
200
|
+
```sh
|
|
201
|
+
mkdir -p ~/.automatic/log
|
|
202
|
+
command -v automatic
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
```crontab
|
|
206
|
+
0 * * * * /usr/local/bin/automatic -c $HOME/.automatic/config/web2markdown.yml >> $HOME/.automatic/log/web2markdown.log 2>&1
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Automatic Ruby runs once and exits; `cron` supplies the schedule.
|
|
210
|
+
|
|
211
|
+
## From a source checkout
|
|
212
|
+
|
|
213
|
+
The normal installation above remains the quickest way to use Automatic Ruby. To
|
|
214
|
+
try the development version or change the source, first follow
|
|
215
|
+
[README's checkout setup](../README.md#from-a-checkout). Every `automatic` above
|
|
216
|
+
then becomes `bundle exec bin/automatic`, run from the checkout directory:
|
|
217
|
+
|
|
218
|
+
```sh
|
|
219
|
+
cd ~/automaticruby
|
|
220
|
+
bundle exec bin/automatic scaffold
|
|
221
|
+
bundle exec bin/automatic -c ~/.automatic/config/web2markdown.yml
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
Step 4 is the step that differs, because a checkout resolves its gems through
|
|
225
|
+
Bundler rather than through RubyGems. Each optional gem is in a Bundler group,
|
|
226
|
+
and the Recipe's groups — `html` and `store`, from step 4 — are selected
|
|
227
|
+
together and installed once:
|
|
228
|
+
|
|
229
|
+
```sh
|
|
230
|
+
bundle config set --local with "html store"
|
|
231
|
+
bundle install
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
`gem install nokogiri` does **not** work here: the gem installs, and the
|
|
235
|
+
checkout still reports it as missing, because it is not in the bundle. That
|
|
236
|
+
difference, the commands that show what the bundle holds, and the same three
|
|
237
|
+
plugins taken step by step through choosing their groups are in
|
|
238
|
+
[`DEPLOYMENT.md`](DEPLOYMENT.md) under "Working out what a Recipe needs, in a
|
|
239
|
+
checkout".
|
|
240
|
+
|
|
241
|
+
## Next
|
|
242
|
+
|
|
243
|
+
The Recipe is ordinary YAML. Add a site, change the Markdown path, or put
|
|
244
|
+
`PublishConsoleLink` at the end to see what the pipeline holds without writing
|
|
245
|
+
anything. The shipped `feed2markdown.yml` and `feed2console.yml` in
|
|
246
|
+
`~/.automatic/config/example` are the same shape over an ordinary feed, for a
|
|
247
|
+
site that publishes one.
|
|
248
|
+
|
|
249
|
+
Taking this same pipeline further — the article bodies, one joined text, and an
|
|
250
|
+
AI service asked one question about it — is
|
|
251
|
+
[`AI_TUTORIAL.md`](AI_TUTORIAL.md).
|
|
252
|
+
|
|
253
|
+
What each plugin does is [`PLUGINS.md`](PLUGINS.md) section 6; installing,
|
|
254
|
+
scheduling and operating a Recipe is [`DEPLOYMENT.md`](DEPLOYMENT.md); writing a
|
|
255
|
+
small plugin of your own is
|
|
256
|
+
[`PLUGIN_DEVELOPMENT.md`](PLUGIN_DEVELOPMENT.md).
|
data/doc/RELEASING.md
ADDED
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
# Releasing Automatic Ruby
|
|
2
|
+
|
|
3
|
+
This is the maintainer runbook for building and publishing the `automatic` gem.
|
|
4
|
+
It describes a manual release. Building and inspecting a gem does not publish
|
|
5
|
+
it; `gem push` does, and must be run only for an explicitly approved release.
|
|
6
|
+
|
|
7
|
+
The governing principles are:
|
|
8
|
+
|
|
9
|
+
- Build locally.
|
|
10
|
+
- Verify before publishing.
|
|
11
|
+
- Keep credentials out of the repository.
|
|
12
|
+
- Publish only an explicitly approved release.
|
|
13
|
+
|
|
14
|
+
Do not restore the historical Jeweler tasks. `automatic.gemspec` is maintained
|
|
15
|
+
by hand, `Rakefile` contains test tasks only, and RubyGems builds the package
|
|
16
|
+
directly from the gemspec.
|
|
17
|
+
|
|
18
|
+
## 1. Prerequisites
|
|
19
|
+
|
|
20
|
+
The releaser needs:
|
|
21
|
+
|
|
22
|
+
- a supported Ruby, currently Ruby 3.3 through 4.0;
|
|
23
|
+
- current Bundler and RubyGems versions compatible with that Ruby;
|
|
24
|
+
- write access to the source repository and permission to create and push tags;
|
|
25
|
+
- ownership of `automatic` on RubyGems.org, or an API key with push permission;
|
|
26
|
+
- access to the RubyGems.org account and its required MFA method.
|
|
27
|
+
|
|
28
|
+
Check the local tools before changing release metadata:
|
|
29
|
+
|
|
30
|
+
```sh
|
|
31
|
+
ruby -v
|
|
32
|
+
gem --version
|
|
33
|
+
bundle --version
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
The release Ruby need not reproduce the complete CI matrix locally. Required
|
|
37
|
+
GitHub Actions checks must be green on Ruby 3.3, 3.4 and 4.0 before publication.
|
|
38
|
+
|
|
39
|
+
## 2. Authentication and credentials
|
|
40
|
+
|
|
41
|
+
RubyGems authenticates publishing operations with an API key. Confirm the
|
|
42
|
+
account is an owner of the package before release; gem ownership grants the
|
|
43
|
+
ability to push versions and yank releases. Use the least privilege needed for
|
|
44
|
+
the release and follow the current RubyGems.org account and API-key guidance.
|
|
45
|
+
|
|
46
|
+
The standard RubyGems sign-in command stores a key in the RubyGems credentials
|
|
47
|
+
file:
|
|
48
|
+
|
|
49
|
+
```sh
|
|
50
|
+
gem signin
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
RubyGems commonly uses `~/.gem/credentials`; with the XDG directory layout it
|
|
54
|
+
may report another user-specific path. Use the path reported by the installed
|
|
55
|
+
RubyGems. The file contains secrets and must be readable and writable only by
|
|
56
|
+
its owner:
|
|
57
|
+
|
|
58
|
+
```sh
|
|
59
|
+
chmod 0600 ~/.gem/credentials
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Never put an API key, password, OTP or credentials file in this repository, the
|
|
63
|
+
Gemfile, the gemspec, a shell script, README, a commit or a GitHub artifact. Do
|
|
64
|
+
not show a real key in an example. Do not pass a key as a literal command-line
|
|
65
|
+
argument, because process listings and shell history may retain it.
|
|
66
|
+
|
|
67
|
+
The gemspec sets `rubygems_mfa_required` to `true`. If RubyGems.org requires
|
|
68
|
+
MFA, complete the interactive prompt from `gem signin` or `gem push`. RubyGems
|
|
69
|
+
also supports `--otp CODE` and the short-lived `GEM_HOST_OTP_CODE` environment
|
|
70
|
+
variable. Do not store an OTP in the repository or a persistent script.
|
|
71
|
+
|
|
72
|
+
Before the release window, verify access through the RubyGems.org web account.
|
|
73
|
+
Do not change owners or create, rotate or revoke production keys as an
|
|
74
|
+
incidental part of this runbook.
|
|
75
|
+
|
|
76
|
+
## 3. Prepare the source version
|
|
77
|
+
|
|
78
|
+
Start from a clean checkout of the release branch and fetch the authoritative
|
|
79
|
+
remote. Review the complete release diff and the top entry in `doc/VERSIONS`.
|
|
80
|
+
|
|
81
|
+
Automatic Ruby uses a calendar version taken from the release date:
|
|
82
|
+
|
|
83
|
+
```text
|
|
84
|
+
X.YY release in year X, month YY
|
|
85
|
+
X.YY.PATCH correction to an earlier release in the same month
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Section 10 of `POLICY.md` is authoritative. A release updates these three
|
|
89
|
+
places together in one release-metadata commit:
|
|
90
|
+
|
|
91
|
+
- `VERSION` contains the package version;
|
|
92
|
+
- `lib/automatic/version.rb` defines `Automatic::VERSION` for the CLI;
|
|
93
|
+
- the current heading in `doc/VERSIONS` contains the same version.
|
|
94
|
+
|
|
95
|
+
`automatic.gemspec` does not contain another version literal. It reads and
|
|
96
|
+
strips the repository-root `VERSION` file. Confirm all three values agree:
|
|
97
|
+
|
|
98
|
+
```sh
|
|
99
|
+
version=$(cat VERSION)
|
|
100
|
+
ruby -Ilib -e "require 'automatic/version'; abort unless Automatic::VERSION == '$version'"
|
|
101
|
+
ruby -e 'spec = Gem::Specification.load("automatic.gemspec")
|
|
102
|
+
abort unless spec.version.to_s == File.read("VERSION").strip'
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Finalize the current unreleased `doc/VERSIONS` entry as a release summary, not
|
|
106
|
+
a development diary. Change `(Release Date: TBD)` to the actual release date.
|
|
107
|
+
Do not add a bullet merely for finalizing the date. Remove or consolidate an
|
|
108
|
+
unreleased bullet only when it does not describe the source being published.
|
|
109
|
+
|
|
110
|
+
## 4. Run release checks
|
|
111
|
+
|
|
112
|
+
Install the default development bundle and run the same default suite used by
|
|
113
|
+
the repository:
|
|
114
|
+
|
|
115
|
+
```sh
|
|
116
|
+
bundle install
|
|
117
|
+
bundle exec rake
|
|
118
|
+
bundle exec ruby -Ilib -e "require 'automatic'"
|
|
119
|
+
bundle exec bin/automatic --version
|
|
120
|
+
bundle exec bin/automatic --help
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Also confirm that all required GitHub Actions checks are green for the release
|
|
124
|
+
commit. Do not publish from an uncommitted or dirty tree:
|
|
125
|
+
|
|
126
|
+
```sh
|
|
127
|
+
git status --short
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## 5. Build the gem
|
|
131
|
+
|
|
132
|
+
Build from the hand-maintained gemspec at the repository root:
|
|
133
|
+
|
|
134
|
+
```sh
|
|
135
|
+
gem build automatic.gemspec
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
This creates `automatic-X.Y.Z.gem` in the current directory. Here and below,
|
|
139
|
+
`X.Y.Z` means the exact value in `VERSION`; the project version may have two or
|
|
140
|
+
three components. The generated archive is ignored by Git and is not source.
|
|
141
|
+
Never commit it.
|
|
142
|
+
|
|
143
|
+
For copy-and-paste-safe inspection commands, capture its exact name:
|
|
144
|
+
|
|
145
|
+
```sh
|
|
146
|
+
version=$(cat VERSION)
|
|
147
|
+
package="automatic-${version}.gem"
|
|
148
|
+
test -f "$package"
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## 6. Inspect package contents
|
|
152
|
+
|
|
153
|
+
First inspect the archive's recorded file list without installing it:
|
|
154
|
+
|
|
155
|
+
```sh
|
|
156
|
+
gem specification "$package" files
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
For a filesystem view, unpack it into a fresh temporary directory:
|
|
160
|
+
|
|
161
|
+
```sh
|
|
162
|
+
inspect_dir=$(mktemp -d)
|
|
163
|
+
gem unpack "$package" --target "$inspect_dir"
|
|
164
|
+
find "$inspect_dir/automatic-$version" -type f | sort
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Confirm that the package contains, at minimum:
|
|
168
|
+
|
|
169
|
+
- `lib/` and `lib/automatic/version.rb`;
|
|
170
|
+
- `bin/automatic`, recorded as the `automatic` executable;
|
|
171
|
+
- the maintained files under `plugins/`;
|
|
172
|
+
- the shipped configuration and assets;
|
|
173
|
+
- maintained documentation, including `README.md` and `doc/RELEASING.md`;
|
|
174
|
+
- `doc/LICENSE.md`, `doc/COPYING` and `doc/COPYING.LESSER`;
|
|
175
|
+
- `VERSION` and `automatic.gemspec`.
|
|
176
|
+
|
|
177
|
+
Confirm that it does not contain:
|
|
178
|
+
|
|
179
|
+
- a generated `.gem` archive or `pkg/` output;
|
|
180
|
+
- `.git`, `.github`, `.bundle`, `Gemfile.lock` or local Bundler directories;
|
|
181
|
+
- `Gemfile`, `Rakefile`, `script/`, `spec/`, `test/` or `vendor/`;
|
|
182
|
+
- credentials, databases, editor files, temporary files or coverage output;
|
|
183
|
+
- documents deleted or superseded in the maintained source tree.
|
|
184
|
+
|
|
185
|
+
The gemspec deliberately derives its list from files visible to Git, then
|
|
186
|
+
filters development and generated paths. Inspect every release archive anyway:
|
|
187
|
+
the build result, rather than the intended filter, is what would be published.
|
|
188
|
+
|
|
189
|
+
## 7. Inspect package metadata
|
|
190
|
+
|
|
191
|
+
Read metadata from the built archive, not only from the source gemspec:
|
|
192
|
+
|
|
193
|
+
```sh
|
|
194
|
+
gem specification "$package" name
|
|
195
|
+
gem specification "$package" version
|
|
196
|
+
gem specification "$package" summary
|
|
197
|
+
gem specification "$package" homepage
|
|
198
|
+
gem specification "$package" metadata
|
|
199
|
+
gem specification "$package" required_ruby_version
|
|
200
|
+
gem specification "$package" licenses
|
|
201
|
+
gem specification "$package" authors
|
|
202
|
+
gem specification "$package" dependencies
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Confirm that:
|
|
206
|
+
|
|
207
|
+
- the name is `automatic` and the version equals `VERSION`;
|
|
208
|
+
- the summary and homepage describe this project;
|
|
209
|
+
- `source_code_uri` is the Automatic Ruby repository;
|
|
210
|
+
- the required Ruby version is `>= 3.3.0`;
|
|
211
|
+
- licenses contain both `GPL-3.0-only` and `LGPL-3.0-only`;
|
|
212
|
+
- the authors are correct;
|
|
213
|
+
- runtime and development dependencies match `automatic.gemspec`, and the
|
|
214
|
+
runtime ones are the framework's own — no gem that belongs to a plugin has
|
|
215
|
+
found its way in ([`POLICY.md`](POLICY.md) section 9.1);
|
|
216
|
+
- `rubygems_mfa_required` is `true`.
|
|
217
|
+
|
|
218
|
+
## 8. Test an isolated local installation
|
|
219
|
+
|
|
220
|
+
Install the archive into a temporary gem home so the smoke test does not load
|
|
221
|
+
the checkout's `lib/` directory. Dependency downloads may require network
|
|
222
|
+
access if they are not already cached.
|
|
223
|
+
|
|
224
|
+
```sh
|
|
225
|
+
gem_home=$(mktemp -d)
|
|
226
|
+
env -u RUBYLIB GEM_HOME="$gem_home" GEM_PATH="$gem_home" \
|
|
227
|
+
gem install "./$package" --no-document
|
|
228
|
+
env -u RUBYLIB GEM_HOME="$gem_home" GEM_PATH="$gem_home" \
|
|
229
|
+
"$gem_home/bin/automatic" --version
|
|
230
|
+
env -u RUBYLIB GEM_HOME="$gem_home" GEM_PATH="$gem_home" \
|
|
231
|
+
"$gem_home/bin/automatic" --help
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
Confirm the installed version equals `VERSION`. Then run a Recipe that needs no
|
|
235
|
+
network and no optional gem, with a temporary home:
|
|
236
|
+
|
|
237
|
+
```sh
|
|
238
|
+
smoke_home=$(mktemp -d)
|
|
239
|
+
mkdir -p "$smoke_home/.automatic/config"
|
|
240
|
+
cat >"$smoke_home/.automatic/config/smoke.yml" <<'YAML'
|
|
241
|
+
plugins:
|
|
242
|
+
- module: SubscriptionText
|
|
243
|
+
config:
|
|
244
|
+
feeds:
|
|
245
|
+
- title: release smoke test
|
|
246
|
+
url: https://example.com/
|
|
247
|
+
- module: PublishConsoleLink
|
|
248
|
+
YAML
|
|
249
|
+
env -u RUBYLIB HOME="$smoke_home" GEM_HOME="$gem_home" GEM_PATH="$gem_home" \
|
|
250
|
+
"$gem_home/bin/automatic" -c smoke.yml
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
The command must print the example link without loading a file from the source
|
|
254
|
+
checkout. Remove the temporary directories when inspection is complete:
|
|
255
|
+
|
|
256
|
+
```sh
|
|
257
|
+
rm -rf "$inspect_dir" "$gem_home" "$smoke_home"
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
## 9. Prepare the release commit and tag
|
|
261
|
+
|
|
262
|
+
After tests and package checks pass, commit the finalized release metadata.
|
|
263
|
+
Wait for required CI on that exact commit. Create an annotated tag named with
|
|
264
|
+
the version prefixed by `v`, as required by the existing version policy:
|
|
265
|
+
|
|
266
|
+
```sh
|
|
267
|
+
version=$(cat VERSION)
|
|
268
|
+
git tag -a "v$version" -m "Release $version"
|
|
269
|
+
git push origin HEAD
|
|
270
|
+
git push origin "v$version"
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
The tag identifies the source used to build the gem. Do not move or force-update
|
|
274
|
+
a release tag after publication. If the tag is wrong before publication, stop
|
|
275
|
+
and correct it openly. If publication has happened, preserve the source history
|
|
276
|
+
and make the correction in a new commit and version.
|
|
277
|
+
|
|
278
|
+
The repository has no established requirement for a GitHub Release separate
|
|
279
|
+
from its Git tag. If maintainers create one, create it from the same immutable
|
|
280
|
+
tag and use the finalized `doc/VERSIONS` entry as its notes. The tag records the
|
|
281
|
+
source; the RubyGems version records the published package. Neither replaces
|
|
282
|
+
the other.
|
|
283
|
+
|
|
284
|
+
## 10. Publish to RubyGems.org
|
|
285
|
+
|
|
286
|
+
Reconfirm approval, the package name and the version immediately before this
|
|
287
|
+
step. The following command performs the real publication to RubyGems.org:
|
|
288
|
+
|
|
289
|
+
```sh
|
|
290
|
+
gem push "automatic-X.Y.Z.gem"
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
Replace the placeholder with the archive already inspected. **Running this
|
|
294
|
+
command actually publishes the gem.** Do not run it for a dry run, a test, or
|
|
295
|
+
merely because the build succeeded. Complete any MFA prompt using the approved
|
|
296
|
+
RubyGems.org account.
|
|
297
|
+
|
|
298
|
+
RubyGems.org does not provide an overwrite operation for an existing name,
|
|
299
|
+
version and platform. A published version is immutable: never rebuild the same
|
|
300
|
+
version and expect a second push to replace it.
|
|
301
|
+
|
|
302
|
+
## 11. Verify publication
|
|
303
|
+
|
|
304
|
+
Open <https://rubygems.org/gems/automatic> and confirm the new version appears.
|
|
305
|
+
Verify its version, both licenses, required Ruby version, dependency metadata,
|
|
306
|
+
homepage and source link against the archive inspected above.
|
|
307
|
+
|
|
308
|
+
Allow for normal index propagation, then install from RubyGems.org into another
|
|
309
|
+
fresh gem home rather than reusing the local-package test:
|
|
310
|
+
|
|
311
|
+
```sh
|
|
312
|
+
verify_home=$(mktemp -d)
|
|
313
|
+
version=X.Y.Z
|
|
314
|
+
GEM_HOME="$verify_home" GEM_PATH="$verify_home" \
|
|
315
|
+
gem install automatic -v "$version" --no-document
|
|
316
|
+
GEM_HOME="$verify_home" GEM_PATH="$verify_home" \
|
|
317
|
+
"$verify_home/bin/automatic" --version
|
|
318
|
+
GEM_HOME="$verify_home" GEM_PATH="$verify_home" \
|
|
319
|
+
"$verify_home/bin/automatic" --help
|
|
320
|
+
rm -rf "$verify_home"
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
The installed version must equal the tag, `VERSION`, `Automatic::VERSION`, the
|
|
324
|
+
`doc/VERSIONS` heading and the version shown by RubyGems.org.
|
|
325
|
+
|
|
326
|
+
## 12. Publication failures and corrections
|
|
327
|
+
|
|
328
|
+
Stop on an error and diagnose it; do not bypass RubyGems security controls.
|
|
329
|
+
|
|
330
|
+
- **Authentication failure:** run `gem signin` for the correct account, check
|
|
331
|
+
the credentials path and permissions, and verify that the key is active and
|
|
332
|
+
has push scope. Never paste the key into source or a committed script.
|
|
333
|
+
- **MFA failure:** use the current OTP for the account and retry only the failed
|
|
334
|
+
push. Check the account's MFA configuration rather than disabling MFA.
|
|
335
|
+
- **Ownership failure:** have an existing owner grant the intended account
|
|
336
|
+
ownership or an appropriately scoped key. Do not publish under another name.
|
|
337
|
+
- **Version already exists:** do not attempt to overwrite it. Determine whether
|
|
338
|
+
it was already published correctly; otherwise fix the source, increment the
|
|
339
|
+
version according to `POLICY.md`, rebuild and repeat every verification step.
|
|
340
|
+
- **Malformed metadata or rejected gem:** fix the gemspec and release metadata,
|
|
341
|
+
increment the version if that version reached RubyGems.org, then rebuild,
|
|
342
|
+
inspect and test the new archive. Do not use `--force` to hide validation.
|
|
343
|
+
|
|
344
|
+
`gem yank automatic -v X.Y.Z` removes a published version from normal index use.
|
|
345
|
+
Yank only for an exceptional serious mispublication, such as exposed secrets or
|
|
346
|
+
a release that must not be installed. It is not the normal correction process,
|
|
347
|
+
does not make the version reusable, and does not erase every downloaded copy.
|
|
348
|
+
The normal response is to correct the problem, bump the version and publish a
|
|
349
|
+
new release. Rotate any exposed secret immediately as well as yanking.
|
|
350
|
+
|
|
351
|
+
Publication state and Git history are separate. Never rewrite a published
|
|
352
|
+
commit, force-push the release branch, or move a release tag to simulate a
|
|
353
|
+
rollback. Preserve a traceable record and publish a corrected version.
|
|
354
|
+
|
|
355
|
+
Future automation may use a publishing mechanism currently supported by
|
|
356
|
+
RubyGems.org, but introducing automatic publication or trusted publishing is a
|
|
357
|
+
separate, explicitly reviewed change.
|
|
358
|
+
|
|
359
|
+
## Official RubyGems references
|
|
360
|
+
|
|
361
|
+
Recheck these official references when preparing a release, because account and
|
|
362
|
+
security behavior can change independently of this repository:
|
|
363
|
+
|
|
364
|
+
- [Publishing your gem](https://guides.rubygems.org/publishing/)
|
|
365
|
+
- [RubyGems command reference](https://guides.rubygems.org/command-reference/)
|
|
366
|
+
- [API key scopes](https://guides.rubygems.org/api-key-scopes/)
|
|
367
|
+
- [MFA requirement opt-in](https://guides.rubygems.org/mfa-requirement-opt-in/)
|
|
368
|
+
- [Removing a published gem](https://guides.rubygems.org/removing-a-published-gem/)
|
|
369
|
+
|
|
370
|
+
## Quick checklist
|
|
371
|
+
|
|
372
|
+
- [ ] `VERSION`, `Automatic::VERSION` and the `doc/VERSIONS` heading agree.
|
|
373
|
+
- [ ] The release date and release summary are finalized.
|
|
374
|
+
- [ ] The working tree is clean and required CI is green.
|
|
375
|
+
- [ ] `bundle exec rake` passes and the gem builds.
|
|
376
|
+
- [ ] Package contents and metadata have been inspected.
|
|
377
|
+
- [ ] GPLv3/LGPLv3, Ruby version and dependencies are correct.
|
|
378
|
+
- [ ] The isolated local install and offline smoke test pass.
|
|
379
|
+
- [ ] The release commit and immutable `vX.Y.Z` tag identify the built source.
|
|
380
|
+
- [ ] Publication has explicit approval and `gem push` succeeds.
|
|
381
|
+
- [ ] RubyGems.org metadata and a clean remote install are verified.
|