shiplog 0.1.1 → 0.3.0
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 +4 -4
- data/CHANGELOG.md +97 -0
- data/README.md +266 -9
- data/app/controllers/shiplog/admin/area_detections_controller.rb +15 -0
- data/app/controllers/shiplog/admin/area_translations_controller.rb +19 -0
- data/app/controllers/shiplog/admin/areas/merges_controller.rb +19 -0
- data/app/controllers/shiplog/admin/areas/moves_controller.rb +13 -0
- data/app/controllers/shiplog/admin/areas_controller.rb +48 -0
- data/app/controllers/shiplog/admin/entries/images/moves_controller.rb +20 -0
- data/app/controllers/shiplog/admin/entries/images_controller.rb +76 -0
- data/app/controllers/shiplog/admin/entries/translations/retranslations_controller.rb +23 -0
- data/app/controllers/shiplog/admin/entries/translations_controller.rb +35 -0
- data/app/controllers/shiplog/admin/entries_controller.rb +9 -3
- data/app/controllers/shiplog/admin/syncs_controller.rb +3 -2
- data/app/controllers/shiplog/changelog_entries_controller.rb +26 -0
- data/app/controllers/shiplog/changelog_feeds_controller.rb +19 -0
- data/app/controllers/shiplog/changelogs_controller.rb +30 -4
- data/app/controllers/shiplog/public_controller.rb +58 -1
- data/app/controllers/shiplog/roadmaps_controller.rb +27 -2
- data/app/frontend/shiplog/admin.css +9 -0
- data/app/frontend/shiplog/shiplog.css +168 -8
- data/app/helpers/shiplog/admin/entries_helper.rb +15 -0
- data/app/helpers/shiplog/callable_helper.rb +12 -0
- data/app/helpers/shiplog/frontend_helper.rb +1 -0
- data/app/helpers/shiplog/permalink_helper.rb +113 -0
- data/app/helpers/shiplog/roadmap_helper.rb +24 -0
- data/app/helpers/shiplog/switcher_helper.rb +101 -0
- data/app/helpers/shiplog/unseen_helper.rb +9 -0
- data/app/jobs/shiplog/detect_areas_job.rb +18 -0
- data/app/jobs/shiplog/refresh_job.rb +1 -0
- data/app/jobs/shiplog/rewrite_entry_job.rb +4 -1
- data/app/jobs/shiplog/scan_changes_job.rb +19 -5
- data/app/jobs/shiplog/sync_job.rb +1 -1
- data/app/jobs/shiplog/translate_area_job.rb +25 -0
- data/app/jobs/shiplog/translate_entry_job.rb +25 -0
- data/app/models/shiplog/area.rb +156 -0
- data/app/models/shiplog/area_detector.rb +146 -0
- data/app/models/shiplog/area_translator.rb +77 -0
- data/app/models/shiplog/change_candidates.rb +39 -4
- data/app/models/shiplog/entry/images.rb +127 -0
- data/app/models/shiplog/entry.rb +218 -2
- data/app/models/shiplog/entry_translation.rb +15 -0
- data/app/models/shiplog/image_fetcher/downloader.rb +98 -0
- data/app/models/shiplog/image_fetcher/sources.rb +41 -0
- data/app/models/shiplog/image_fetcher.rb +72 -0
- data/app/models/shiplog/image_type.rb +111 -0
- data/app/models/shiplog/pull_request.rb +4 -0
- data/app/models/shiplog/pull_request_upgrade.rb +5 -0
- data/app/models/shiplog/rewriter/prompt.rb +57 -9
- data/app/models/shiplog/rewriter.rb +50 -13
- data/app/models/shiplog/setting.rb +19 -0
- data/app/models/shiplog/sync/pull_request_importer.rb +23 -1
- data/app/models/shiplog/sync/repository_sync.rb +2 -0
- data/app/models/shiplog/sync/upgrade_detector.rb +203 -0
- data/app/models/shiplog/translator/prompt.rb +40 -0
- data/app/models/shiplog/translator.rb +99 -0
- data/app/views/layouts/shiplog/application.html.erb +4 -0
- data/app/views/shiplog/_empty.html.erb +1 -1
- data/app/views/shiplog/_page_header.html.erb +2 -1
- data/app/views/shiplog/_pagination.html.erb +3 -3
- data/app/views/shiplog/_switcher.html.erb +13 -0
- data/app/views/shiplog/admin/_nav.html.erb +2 -1
- data/app/views/shiplog/admin/areas/index.html.erb +78 -0
- data/app/views/shiplog/admin/entries/_entry_row.html.erb +4 -0
- data/app/views/shiplog/admin/entries/_images.html.erb +41 -0
- data/app/views/shiplog/admin/entries/_translations.html.erb +42 -0
- data/app/views/shiplog/admin/entries/edit.html.erb +12 -0
- data/app/views/shiplog/changelog_entries/show.html.erb +25 -0
- data/app/views/shiplog/changelog_feeds/show.atom.builder +24 -0
- data/app/views/shiplog/changelogs/_areas.html.erb +8 -0
- data/app/views/shiplog/changelogs/_entry.html.erb +2 -5
- data/app/views/shiplog/changelogs/_entry_content.html.erb +10 -0
- data/app/views/shiplog/changelogs/_search.html.erb +18 -0
- data/app/views/shiplog/changelogs/show.html.erb +20 -5
- data/app/views/shiplog/roadmaps/_card.html.erb +14 -3
- data/app/views/shiplog/roadmaps/_column.html.erb +4 -4
- data/app/views/shiplog/roadmaps/_shipped.html.erb +15 -0
- data/app/views/shiplog/roadmaps/show.html.erb +11 -2
- data/config/admin_routes.rb +16 -0
- data/config/locales/en.yml +105 -0
- data/config/locales/es.yml +223 -0
- data/config/routes.rb +5 -0
- data/db/migrate/20261027000001_create_shiplog_pull_request_upgrades.rb +30 -0
- data/db/migrate/20261027000002_shiplog_0_3_0.rb +105 -0
- data/lib/generators/shiplog/install/install_generator.rb +29 -1
- data/lib/generators/shiplog/install/templates/initializer.rb.tt +82 -1
- data/lib/shiplog/classes.rb +29 -1
- data/lib/shiplog/configuration.rb +245 -3
- data/lib/shiplog/engine.rb +3 -0
- data/lib/shiplog/github/client.rb +42 -9
- data/lib/shiplog/github/errors.rb +3 -0
- data/lib/shiplog/github/queries.rb +44 -1
- data/lib/shiplog/version.rb +1 -1
- data/lib/shiplog.rb +73 -0
- data/lib/tasks/shiplog.rake +29 -0
- metadata +48 -1
|
@@ -67,6 +67,37 @@
|
|
|
67
67
|
.shiplog-page__title { font-size: 1.875rem; line-height: 1.2; margin: 0; }
|
|
68
68
|
.shiplog-page__lede { color: var(--sl-muted); margin: 0.5rem 0 0; }
|
|
69
69
|
|
|
70
|
+
/* Switcher: Roadmap / Changelog / host pages. It can sit outside .shiplog-page (host pages), so it
|
|
71
|
+
falls back to the host-safe palette when the page variables are not inherited. */
|
|
72
|
+
.shiplog-switcher {
|
|
73
|
+
--sw-border: var(--sl-border, var(--shiplog-border, color-mix(in srgb, currentColor 22%, transparent)));
|
|
74
|
+
--sw-card: var(--sl-card, var(--shiplog-card, color-mix(in srgb, currentColor 5%, transparent)));
|
|
75
|
+
--sw-muted: var(--sl-muted, var(--shiplog-muted, color-mix(in srgb, currentColor 65%, transparent)));
|
|
76
|
+
--sw-accent: var(--sl-accent, var(--shiplog-accent, #2f81f7));
|
|
77
|
+
background: var(--sw-card);
|
|
78
|
+
border: 1px solid var(--sw-border);
|
|
79
|
+
border-radius: 999px;
|
|
80
|
+
display: inline-flex;
|
|
81
|
+
flex-wrap: wrap;
|
|
82
|
+
font-size: 0.875rem;
|
|
83
|
+
font-weight: 500;
|
|
84
|
+
gap: 0.25rem;
|
|
85
|
+
margin-top: var(--sl-space, 1rem);
|
|
86
|
+
padding: 0.25rem;
|
|
87
|
+
}
|
|
88
|
+
.shiplog-switcher__link {
|
|
89
|
+
border-radius: 999px;
|
|
90
|
+
color: var(--sw-muted);
|
|
91
|
+
padding: 0.25rem 1rem;
|
|
92
|
+
text-decoration: none;
|
|
93
|
+
transition: background-color 0.15s, color 0.15s;
|
|
94
|
+
}
|
|
95
|
+
.shiplog-switcher__link:hover { color: inherit; }
|
|
96
|
+
.shiplog-switcher__link:focus-visible { outline: 2px solid var(--sw-accent); outline-offset: 2px; }
|
|
97
|
+
.shiplog-switcher__link--current,
|
|
98
|
+
.shiplog-switcher__link--current:hover { background: var(--sw-accent); color: #fff; }
|
|
99
|
+
@media (prefers-reduced-motion: reduce) { .shiplog-switcher__link { transition: none; } }
|
|
100
|
+
|
|
70
101
|
.shiplog-empty,
|
|
71
102
|
.shiplog-column__empty { color: var(--sl-muted); margin: 0; }
|
|
72
103
|
|
|
@@ -77,17 +108,21 @@
|
|
|
77
108
|
grid-template-columns: repeat(auto-fit, minmax(min(16rem, 100%), 1fr));
|
|
78
109
|
}
|
|
79
110
|
|
|
111
|
+
/* A rule on top and a marker carry the column's place in time: in progress is solid, coming soon
|
|
112
|
+
is the accent, later is neutral. */
|
|
80
113
|
.shiplog-column {
|
|
81
114
|
--sl-dot: var(--sl-muted);
|
|
115
|
+
--sl-rule: var(--sl-border);
|
|
116
|
+
border-top: 2px solid var(--sl-rule);
|
|
82
117
|
min-width: 0;
|
|
118
|
+
padding-top: 1rem;
|
|
83
119
|
}
|
|
84
|
-
.shiplog-column--in-progress { --sl-dot: #1a7f37; }
|
|
85
|
-
.shiplog-column--coming-soon { --sl-dot: var(--sl-accent); }
|
|
86
|
-
.shiplog-column--later { --sl-dot: var(--sl-muted); }
|
|
120
|
+
.shiplog-column--in-progress { --sl-dot: #1a7f37; --sl-rule: var(--sl-dot); }
|
|
121
|
+
.shiplog-column--coming-soon { --sl-dot: var(--sl-accent); --sl-rule: var(--sl-dot); }
|
|
122
|
+
.shiplog-column--later { --sl-dot: var(--sl-muted); --sl-rule: var(--sl-border); }
|
|
87
123
|
|
|
88
124
|
.shiplog-column__header {
|
|
89
125
|
align-items: baseline;
|
|
90
|
-
border-bottom: 1px solid var(--sl-border);
|
|
91
126
|
display: flex;
|
|
92
127
|
flex-wrap: wrap;
|
|
93
128
|
gap: 0 0.5rem;
|
|
@@ -95,13 +130,14 @@
|
|
|
95
130
|
padding-bottom: 0.75rem;
|
|
96
131
|
}
|
|
97
132
|
.shiplog-column__title { align-items: center; display: flex; font-size: 1.125rem; gap: 0.5rem; margin: 0; }
|
|
98
|
-
.shiplog-
|
|
133
|
+
.shiplog-column__marker {
|
|
99
134
|
background: var(--sl-dot);
|
|
100
135
|
border-radius: 50%;
|
|
101
|
-
|
|
136
|
+
flex-shrink: 0;
|
|
102
137
|
height: 0.625rem;
|
|
103
138
|
width: 0.625rem;
|
|
104
139
|
}
|
|
140
|
+
.shiplog-column__marker--in-progress { box-shadow: 0 0 0 4px color-mix(in srgb, var(--sl-dot) 20%, transparent); }
|
|
105
141
|
.shiplog-column__count {
|
|
106
142
|
background: var(--sl-card);
|
|
107
143
|
border: 1px solid var(--sl-border);
|
|
@@ -116,7 +152,8 @@
|
|
|
116
152
|
.shiplog-entries { display: grid; gap: 0.75rem; list-style: none; margin: 0; padding: 0; }
|
|
117
153
|
|
|
118
154
|
.shiplog-card,
|
|
119
|
-
.shiplog-entry
|
|
155
|
+
.shiplog-entry,
|
|
156
|
+
.shiplog-permalink {
|
|
120
157
|
background: var(--sl-card);
|
|
121
158
|
border: 1px solid var(--sl-border);
|
|
122
159
|
border-radius: var(--sl-radius);
|
|
@@ -132,6 +169,58 @@
|
|
|
132
169
|
.shiplog-entry__body p { margin: 0.25rem 0; }
|
|
133
170
|
.shiplog-card__body { color: var(--sl-muted); font-size: 0.9375rem; }
|
|
134
171
|
|
|
172
|
+
.shiplog-card__meta {
|
|
173
|
+
align-items: center;
|
|
174
|
+
color: var(--sl-muted);
|
|
175
|
+
display: flex;
|
|
176
|
+
flex-wrap: wrap;
|
|
177
|
+
font-size: 0.75rem;
|
|
178
|
+
font-weight: 500;
|
|
179
|
+
gap: 0.5rem;
|
|
180
|
+
margin: 0.75rem 0 0;
|
|
181
|
+
}
|
|
182
|
+
.shiplog-card__tag {
|
|
183
|
+
border: 1px solid var(--sl-border);
|
|
184
|
+
border-radius: 0.375rem;
|
|
185
|
+
padding: 0 0.5rem;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/* Recently shipped: a title, a link to the full changelog, then a grid of the latest entries */
|
|
189
|
+
.shiplog-shipped {
|
|
190
|
+
align-items: baseline;
|
|
191
|
+
border-top: 1px solid var(--sl-border);
|
|
192
|
+
display: grid;
|
|
193
|
+
gap: 0.5rem 1rem;
|
|
194
|
+
grid-template-columns: 1fr auto;
|
|
195
|
+
margin-top: calc(var(--sl-space) * 2.5);
|
|
196
|
+
padding-top: calc(var(--sl-space) * 1.5);
|
|
197
|
+
}
|
|
198
|
+
.shiplog-shipped__title { font-size: 1.125rem; margin: 0; }
|
|
199
|
+
.shiplog-shipped__list {
|
|
200
|
+
display: grid;
|
|
201
|
+
gap: var(--sl-space) calc(var(--sl-space) * 1.5);
|
|
202
|
+
grid-column: 1 / -1;
|
|
203
|
+
grid-template-columns: repeat(auto-fit, minmax(min(12rem, 100%), 1fr));
|
|
204
|
+
list-style: none;
|
|
205
|
+
margin: 0;
|
|
206
|
+
padding: 0;
|
|
207
|
+
}
|
|
208
|
+
.shiplog-shipped__item { display: grid; gap: 0.25rem; align-content: start; }
|
|
209
|
+
.shiplog-shipped__date { color: var(--sl-muted); font-size: 0.75rem; }
|
|
210
|
+
.shiplog-shipped__link { color: inherit; font-weight: 600; text-decoration: none; }
|
|
211
|
+
.shiplog-shipped__link:hover { color: var(--sl-accent); }
|
|
212
|
+
.shiplog-shipped__link--all { color: var(--sl-accent); font-size: 0.875rem; font-weight: 500; }
|
|
213
|
+
|
|
214
|
+
.shiplog-footnote {
|
|
215
|
+
border-top: 1px solid var(--sl-border);
|
|
216
|
+
color: var(--sl-muted);
|
|
217
|
+
font-size: 0.875rem;
|
|
218
|
+
margin: calc(var(--sl-space) * 2) 0 0;
|
|
219
|
+
padding-top: var(--sl-space);
|
|
220
|
+
text-align: center;
|
|
221
|
+
}
|
|
222
|
+
.shiplog-footnote__link { color: var(--sl-accent); font-weight: 500; }
|
|
223
|
+
|
|
135
224
|
/* Changelog: months hang off a vertical line */
|
|
136
225
|
.shiplog-months { display: grid; gap: calc(var(--sl-space) * 2); }
|
|
137
226
|
|
|
@@ -153,10 +242,14 @@
|
|
|
153
242
|
width: 0.75rem;
|
|
154
243
|
}
|
|
155
244
|
|
|
156
|
-
.shiplog-entry
|
|
245
|
+
.shiplog-entry,
|
|
246
|
+
.shiplog-permalink { align-items: center; display: flex; flex-wrap: wrap; gap: 0.25rem 0.625rem; }
|
|
157
247
|
.shiplog-entry__title,
|
|
158
248
|
.shiplog-entry__body { flex-basis: 100%; }
|
|
159
249
|
.shiplog-entry__title { font-size: 1rem; margin: 0.25rem 0 0; }
|
|
250
|
+
.shiplog-entry__title a { color: inherit; text-decoration: none; }
|
|
251
|
+
.shiplog-entry__title a:hover { color: var(--sl-accent); text-decoration: underline; }
|
|
252
|
+
.shiplog-entry__title a:focus-visible { outline: 2px solid var(--sl-accent); outline-offset: 2px; }
|
|
160
253
|
.shiplog-entry__body { color: var(--sl-muted); }
|
|
161
254
|
.shiplog-entry__date { color: var(--sl-muted); font-size: 0.8125rem; }
|
|
162
255
|
|
|
@@ -168,6 +261,9 @@
|
|
|
168
261
|
font-weight: 600;
|
|
169
262
|
padding: 0 0.5rem;
|
|
170
263
|
}
|
|
264
|
+
.shiplog-permalink .shiplog-entry__title { font-size: 1.5rem; }
|
|
265
|
+
.shiplog-permalink--feature { border-left: 4px solid var(--sl-accent); }
|
|
266
|
+
.shiplog-permalink--feature .shiplog-entry__category { background: var(--sl-accent); border-color: var(--sl-accent); color: #fff; }
|
|
171
267
|
.shiplog-entry--feature { border-left: 4px solid var(--sl-accent); padding: var(--sl-space) calc(var(--sl-space) * 1.25); }
|
|
172
268
|
.shiplog-entry--feature .shiplog-entry__title { font-size: 1.25rem; }
|
|
173
269
|
.shiplog-entry--feature .shiplog-entry__category { background: var(--sl-accent); border-color: var(--sl-accent); color: #fff; }
|
|
@@ -176,6 +272,70 @@
|
|
|
176
272
|
.shiplog-pagination__link { color: var(--sl-accent); }
|
|
177
273
|
.shiplog-pagination__link--older { margin-left: auto; }
|
|
178
274
|
|
|
275
|
+
.shiplog-permalink__back { color: var(--sl-accent); display: inline-block; margin-top: var(--sl-space); }
|
|
276
|
+
.shiplog-permalink__nav {
|
|
277
|
+
display: flex;
|
|
278
|
+
flex-wrap: wrap;
|
|
279
|
+
gap: var(--sl-space);
|
|
280
|
+
justify-content: space-between;
|
|
281
|
+
margin-top: calc(var(--sl-space) * 2);
|
|
282
|
+
}
|
|
283
|
+
.shiplog-permalink__nav-link { color: var(--sl-accent); }
|
|
284
|
+
.shiplog-permalink__nav-link--older { margin-left: auto; text-align: right; }
|
|
285
|
+
.shiplog-permalink__back:focus-visible,
|
|
286
|
+
.shiplog-permalink__nav-link:focus-visible { outline: 2px solid var(--sl-accent); outline-offset: 2px; }
|
|
287
|
+
|
|
288
|
+
.shiplog-entry__area {
|
|
289
|
+
color: var(--sl-muted);
|
|
290
|
+
font-size: 0.75rem;
|
|
291
|
+
font-weight: 600;
|
|
292
|
+
letter-spacing: 0.02em;
|
|
293
|
+
text-transform: uppercase;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
.shiplog-entry__images { display: grid; flex-basis: 100%; gap: 0.75rem; margin-top: 0.5rem; }
|
|
297
|
+
.shiplog-entry__image { border: 1px solid var(--sl-border); border-radius: 0.5rem; display: block; height: auto; max-width: 100%; }
|
|
298
|
+
|
|
299
|
+
.shiplog-areas { display: flex; flex-wrap: wrap; gap: 0.5rem; margin: var(--sl-space) 0; }
|
|
300
|
+
.shiplog-areas__link {
|
|
301
|
+
border: 1px solid var(--sl-border);
|
|
302
|
+
border-radius: 1rem;
|
|
303
|
+
color: var(--sl-fg);
|
|
304
|
+
font-size: 0.875rem;
|
|
305
|
+
padding: 0.125rem 0.75rem;
|
|
306
|
+
text-decoration: none;
|
|
307
|
+
}
|
|
308
|
+
.shiplog-areas__link:hover { border-color: var(--sl-accent); }
|
|
309
|
+
.shiplog-areas__link:focus-visible { outline: 2px solid var(--sl-accent); outline-offset: 2px; }
|
|
310
|
+
.shiplog-areas__link--current { background: var(--sl-accent); border-color: var(--sl-accent); color: #fff; }
|
|
311
|
+
|
|
312
|
+
.shiplog-search { display: flex; flex-wrap: wrap; gap: 0.5rem; margin: var(--sl-space) 0; }
|
|
313
|
+
.shiplog-search__input {
|
|
314
|
+
flex: 1 1 14rem;
|
|
315
|
+
min-width: 0;
|
|
316
|
+
font: inherit;
|
|
317
|
+
color: inherit;
|
|
318
|
+
background: var(--sl-card);
|
|
319
|
+
border: 1px solid var(--sl-border);
|
|
320
|
+
border-radius: var(--sl-radius);
|
|
321
|
+
padding: 0.4rem 0.75rem;
|
|
322
|
+
}
|
|
323
|
+
.shiplog-search__input:focus-visible,
|
|
324
|
+
.shiplog-search__button:focus-visible,
|
|
325
|
+
.shiplog-search__clear:focus-visible { outline: 2px solid var(--sl-accent); outline-offset: 2px; }
|
|
326
|
+
.shiplog-search__button {
|
|
327
|
+
font: inherit;
|
|
328
|
+
color: #fff;
|
|
329
|
+
background: var(--sl-accent);
|
|
330
|
+
border: 1px solid var(--sl-accent);
|
|
331
|
+
border-radius: var(--sl-radius);
|
|
332
|
+
padding: 0.4rem 1rem;
|
|
333
|
+
cursor: pointer;
|
|
334
|
+
}
|
|
335
|
+
.shiplog-search__summary { color: var(--sl-muted); margin: 0 0 var(--sl-space); }
|
|
336
|
+
.shiplog-search__clear { color: var(--sl-accent); margin-left: 0.5rem; }
|
|
337
|
+
.shiplog-search__empty { color: var(--sl-muted); margin: 0; }
|
|
338
|
+
|
|
179
339
|
@media (max-width: 40rem) {
|
|
180
340
|
.shiplog-page { padding: var(--sl-space) 0.75rem; }
|
|
181
341
|
.shiplog-page__title { font-size: 1.5rem; }
|
|
@@ -19,6 +19,21 @@ module Shiplog
|
|
|
19
19
|
entry.title.presence || source_title(entry.subject)
|
|
20
20
|
end
|
|
21
21
|
|
|
22
|
+
# The URL of an image's thumbnail: a 200x200 variant when the host can make them (image_processing and a
|
|
23
|
+
# processor, see Shiplog.image_variants?), else the original (the stylesheet sizes it).
|
|
24
|
+
def image_thumbnail_path(image)
|
|
25
|
+
source = Shiplog.image_variants? && image.variable? ? image.variant(resize_to_limit: [ 200, 200 ]) : image
|
|
26
|
+
polymorphic_path(source)
|
|
27
|
+
rescue ActionController::UrlGenerationError, NoMethodError, ArgumentError
|
|
28
|
+
nil
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# "Spanish (es)" for a locale of config.locales; the code alone when Shiplog has no name for the language.
|
|
32
|
+
def translation_language_label(locale)
|
|
33
|
+
name = Shiplog::Translator.language_name(locale)
|
|
34
|
+
name == locale.to_s ? name : "#{name} (#{locale})"
|
|
35
|
+
end
|
|
36
|
+
|
|
22
37
|
def entries_filter_path(status: nil, kind: nil, page: nil)
|
|
23
38
|
root_path({ status:, kind:, page: }.compact_blank)
|
|
24
39
|
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
module Shiplog
|
|
2
|
+
module CallableHelper
|
|
3
|
+
# A config value that is a String, or a callable evaluated in the view so it can use route helpers
|
|
4
|
+
# and I18n (`-> { main_app.news_path }`, `-> { t("...") }`).
|
|
5
|
+
def shiplog_resolve(value)
|
|
6
|
+
case value
|
|
7
|
+
when Proc then instance_exec(&value)
|
|
8
|
+
else value.respond_to?(:call) ? value.call : value
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
module Shiplog
|
|
2
2
|
module FrontendHelper
|
|
3
3
|
def shiplog_stylesheet_tag(name)
|
|
4
|
+
@_shiplog_stylesheet_emitted = true if name == "shiplog" # shiplog_switcher then skips its own link
|
|
4
5
|
tag.link rel: "stylesheet", href: shiplog_asset_path("#{name}.css"), nonce: content_security_policy_nonce
|
|
5
6
|
end
|
|
6
7
|
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
module Shiplog
|
|
2
|
+
# Markup helpers of the permalink pages, the Atom feed and the entries' screenshots.
|
|
3
|
+
module PermalinkHelper
|
|
4
|
+
DESCRIPTION_LENGTH = 160
|
|
5
|
+
|
|
6
|
+
# The path of an entry's permalink page, or nil when the entry has no slug the route accepts (a link is
|
|
7
|
+
# an extra, never a reason to fail the page).
|
|
8
|
+
def shiplog_entry_path(entry)
|
|
9
|
+
changelog_entry_path(entry.slug) if entry.permalink?
|
|
10
|
+
rescue ActionController::UrlGenerationError
|
|
11
|
+
nil
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# The absolute URL of a named engine route: config.canonical_url plus the route's path (which carries the
|
|
15
|
+
# engine's mount prefix) when it is set, the request's own origin otherwise.
|
|
16
|
+
# shiplog_absolute_url(:changelog_feed) # => "https://trackberry.com/changelog.atom"
|
|
17
|
+
def shiplog_absolute_url(route, *args)
|
|
18
|
+
canonical = Shiplog.config.canonical_url
|
|
19
|
+
canonical ? "#{canonical}#{public_send("#{route}_path", *args)}" : public_send("#{route}_url", *args)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# The absolute URL of an entry's permalink page, or nil when it has no valid slug.
|
|
23
|
+
def shiplog_entry_url(entry)
|
|
24
|
+
shiplog_absolute_url(:changelog_entry, entry.slug) if shiplog_entry_path(entry)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# The authority of the feed's tag URIs: the canonical host, else the request's.
|
|
28
|
+
def shiplog_tag_host
|
|
29
|
+
canonical = Shiplog.config.canonical_url
|
|
30
|
+
canonical ? URI.parse(canonical).host : request.host
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# A link to an entry's permalink page, or the plain content in a span when it has none.
|
|
34
|
+
def shiplog_entry_link(entry, name = nil, html_options = {}, &block)
|
|
35
|
+
name, html_options = capture(&block), name || {} if block
|
|
36
|
+
path = shiplog_entry_path(entry)
|
|
37
|
+
path ? link_to(name, path, html_options) : content_tag(:span, name, html_options.slice(:class))
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# The id of a month's section on the changelog list.
|
|
41
|
+
def shiplog_month_anchor(date) = "shiplog-month-#{date.strftime("%Y-%m")}"
|
|
42
|
+
|
|
43
|
+
# The body, on one line and cut for a meta description. Plain text: tag helpers escape it.
|
|
44
|
+
def shiplog_entry_description(entry)
|
|
45
|
+
truncate(entry.display_body.to_s.squish, length: DESCRIPTION_LENGTH, separator: " ")
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# The Open Graph meta tags of a permalink page, for `content_for :shiplog_og`.
|
|
49
|
+
def shiplog_og_tags(entry, url:)
|
|
50
|
+
tags = {
|
|
51
|
+
"og:title" => entry.display_title, "og:description" => shiplog_entry_description(entry),
|
|
52
|
+
"og:type" => "article", "og:url" => url, "og:image" => shiplog_entry_og_image(entry)
|
|
53
|
+
}
|
|
54
|
+
safe_join(tags.compact_blank.map { |property, content| tag.meta(property:, content:) }, "\n")
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# The absolute URL of the image to show when the page is shared (the first image), or nil.
|
|
58
|
+
def shiplog_entry_og_image(entry)
|
|
59
|
+
image = shiplog_images(entry).first
|
|
60
|
+
shiplog_image_url(image) if image
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# The HTML of an entry in the Atom feed: its text, then its images with absolute URLs.
|
|
64
|
+
def shiplog_feed_content(entry)
|
|
65
|
+
images = shiplog_images(entry).filter_map do |image|
|
|
66
|
+
url = shiplog_image_url(image)
|
|
67
|
+
tag.p(tag.img(src: url, alt: entry.image_alt(image), **shiplog_image_size(image))) if url
|
|
68
|
+
end
|
|
69
|
+
safe_join([ simple_format(h(entry.display_body)), *images ], "\n")
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# An entry's images in order, or none while screenshots are off.
|
|
73
|
+
def shiplog_images(entry)
|
|
74
|
+
Shiplog.images? ? entry.ordered_images : []
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# The images of an entry under its body: `limit` of them (all by default). Nil when it has none to show.
|
|
78
|
+
def shiplog_entry_images(entry, limit: nil)
|
|
79
|
+
images = shiplog_images(entry)
|
|
80
|
+
images = images.first(limit) if limit
|
|
81
|
+
tags = images.filter_map do |image|
|
|
82
|
+
path = shiplog_image_path(image)
|
|
83
|
+
tag.img(src: path, alt: entry.image_alt(image), loading: "lazy", class: shiplog_class(:entry_image), **shiplog_image_size(image)) if path
|
|
84
|
+
end
|
|
85
|
+
tag.div(safe_join(tags), class: shiplog_class(:entry_images)) if tags.any?
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Active Storage's routes belong to the host: url_for reaches them through main_app (see HostRoutesHelper).
|
|
89
|
+
# Nil when the host can't build the route.
|
|
90
|
+
def shiplog_image_path(image)
|
|
91
|
+
polymorphic_path(image)
|
|
92
|
+
rescue ActionController::UrlGenerationError, NoMethodError, ArgumentError
|
|
93
|
+
nil
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# The image's absolute URL: config.canonical_url plus its path when set, the request's origin otherwise.
|
|
97
|
+
def shiplog_image_url(image)
|
|
98
|
+
canonical = Shiplog.config.canonical_url
|
|
99
|
+
return polymorphic_url(image) unless canonical
|
|
100
|
+
|
|
101
|
+
path = shiplog_image_path(image)
|
|
102
|
+
"#{canonical}#{path}" if path
|
|
103
|
+
rescue ActionController::UrlGenerationError, NoMethodError, ArgumentError
|
|
104
|
+
nil
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# width and height from the blob's analysis, when it has run and found them.
|
|
108
|
+
def shiplog_image_size(image)
|
|
109
|
+
metadata = image.blob.metadata
|
|
110
|
+
{ width: metadata[:width], height: metadata[:height] }.select { |_, value| value.to_i.positive? }
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
module Shiplog
|
|
2
|
+
module RoadmapHelper
|
|
3
|
+
include Shiplog::SwitcherHelper
|
|
4
|
+
|
|
5
|
+
# English texts of the roadmap columns, used when the host's locale has no shiplog.columns translations.
|
|
6
|
+
COLUMN_DEFAULTS = {
|
|
7
|
+
in_progress: { title: "In progress", description: "What we're building now" },
|
|
8
|
+
coming_soon: { title: "Coming soon", description: "Planned next" },
|
|
9
|
+
later: { title: "Later", description: "On our radar" }
|
|
10
|
+
}.freeze
|
|
11
|
+
|
|
12
|
+
def shiplog_column_defaults = COLUMN_DEFAULTS
|
|
13
|
+
|
|
14
|
+
# The configured label of a repository (config.repository_labels, matched case-insensitively), or nil.
|
|
15
|
+
def shiplog_repository_label(repository)
|
|
16
|
+
labels = Shiplog.config.repository_labels
|
|
17
|
+
label = labels[repository.full_name] || labels.find { |name, _| name.casecmp?(repository.full_name) }&.last
|
|
18
|
+
shiplog_resolve(label).presence
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# The "suggest a feature" target (String or callable), or nil.
|
|
22
|
+
def shiplog_feedback_path = shiplog_safe_url(shiplog_resolve(Shiplog.config.feedback_path))
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
module Shiplog
|
|
2
|
+
# The Roadmap / Changelog / extra pages switcher. The default page header renders it, and a host adds
|
|
3
|
+
# `helper Shiplog::SwitcherHelper` to render it on its own pages (e.g. a news page): `<%= shiplog_switcher %>`.
|
|
4
|
+
# It uses the engine's own route helpers, or the proxy of the host's mount (whatever its `as:`), so it works in
|
|
5
|
+
# main-app views too.
|
|
6
|
+
module SwitcherHelper
|
|
7
|
+
include Shiplog::StylingHelper
|
|
8
|
+
include Shiplog::CallableHelper
|
|
9
|
+
|
|
10
|
+
# URLs already warned about. Capped, since a callable path could return a new bad URL on every request.
|
|
11
|
+
WARNED = Set.new
|
|
12
|
+
MAX_WARNED = 100
|
|
13
|
+
private_constant :WARNED, :MAX_WARNED
|
|
14
|
+
|
|
15
|
+
def self.reset_warnings! = WARNED.clear
|
|
16
|
+
|
|
17
|
+
NotMounted = Class.new(StandardError)
|
|
18
|
+
|
|
19
|
+
# The name the host gave the engine's mount in its routes (`mount Shiplog::Engine => "/", as: :updates`; the
|
|
20
|
+
# default is "shiplog"), which is the name of its route proxy in host views. Found by looking for the route
|
|
21
|
+
# of config/routes.rb whose Rack app is the engine (the Constraints wrapper's `app`; Rails::Engine is what
|
|
22
|
+
# `mount` stores) and remembered while that route exists.
|
|
23
|
+
def self.mount_name
|
|
24
|
+
cached = @mount_name
|
|
25
|
+
return cached if cached && Rails.application.routes.named_routes.key?(cached)
|
|
26
|
+
|
|
27
|
+
route = Rails.application.routes.routes.find { |candidate| candidate.app.try(:app) == Shiplog::Engine }
|
|
28
|
+
raise NotMounted, "Shiplog::Engine is not mounted in the host's routes: add `mount Shiplog::Engine => \"/\"` to config/routes.rb" unless route&.name
|
|
29
|
+
|
|
30
|
+
@mount_name = route.name
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def self.reset_mount_name! = (@mount_name = nil)
|
|
34
|
+
|
|
35
|
+
# The switcher for host pages: the bundled stylesheet (once per page, and not when the engine page already
|
|
36
|
+
# linked it; unless config.stylesheet is false) and the nav.
|
|
37
|
+
def shiplog_switcher
|
|
38
|
+
stylesheet = if Shiplog.config.stylesheet && !@_shiplog_stylesheet_emitted
|
|
39
|
+
@_shiplog_stylesheet_emitted = true
|
|
40
|
+
tag.link(rel: "stylesheet", nonce: content_security_policy_nonce,
|
|
41
|
+
href: shiplog_routes.frontend_path(version: Shiplog::VERSION, name: "shiplog.css"))
|
|
42
|
+
end
|
|
43
|
+
safe_join([ stylesheet, render("shiplog/switcher") ])
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# The nav's aria-label: a plain String (never html_safe markup, never a translation-missing span).
|
|
47
|
+
def shiplog_switcher_label = String.new(I18n.t("shiplog.switcher.label", default: "Product updates"))
|
|
48
|
+
|
|
49
|
+
# [{ label:, path:, current: }] in display order. A path with an unsafe scheme is nil (rendered as text).
|
|
50
|
+
# Only the page with the longest matching path is current.
|
|
51
|
+
def shiplog_switcher_pages
|
|
52
|
+
pages = [
|
|
53
|
+
{ label: t("shiplog.switcher.roadmap", default: "Roadmap"), path: shiplog_routes.roadmap_path },
|
|
54
|
+
{ label: t("shiplog.switcher.changelog", default: "Changelog"), path: shiplog_routes.changelog_path },
|
|
55
|
+
*Shiplog.config.extra_pages.map { |page| { label: shiplog_resolve(page[:label]), path: shiplog_safe_url(shiplog_resolve(page[:path])) } }
|
|
56
|
+
]
|
|
57
|
+
lengths = pages.map { |page| shiplog_match_length(page[:path]) }
|
|
58
|
+
best = lengths.compact.max
|
|
59
|
+
current = best && lengths.index(best)
|
|
60
|
+
pages.each_with_index.map { |page, index| page.merge(current: index == current) }
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# The URL when it is safe to link (see Configuration.safe_url?); otherwise nil, with a warning logged once.
|
|
64
|
+
def shiplog_safe_url(url)
|
|
65
|
+
return if url.blank?
|
|
66
|
+
return url.to_s if Shiplog::Configuration.safe_url?(url)
|
|
67
|
+
|
|
68
|
+
if WARNED.size < MAX_WARNED && WARNED.add?(url.to_s)
|
|
69
|
+
Rails.logger.warn("[Shiplog] Not linking #{url.inspect}: only paths, http(s) and mailto: URLs are allowed")
|
|
70
|
+
end
|
|
71
|
+
nil
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
private
|
|
75
|
+
|
|
76
|
+
# Where the engine's route helpers live: the view itself inside the engine's own pages, otherwise (host pages)
|
|
77
|
+
# the proxy the host's mount defined, whatever its name.
|
|
78
|
+
def shiplog_routes
|
|
79
|
+
return self if controller.try(:_routes) == Shiplog::Engine.routes
|
|
80
|
+
|
|
81
|
+
public_send(SwitcherHelper.mount_name)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Length of the link's path when it contains the request path (equal, or a parent segment), else nil.
|
|
85
|
+
# An absolute URL counts only for http(s) and the request's own host.
|
|
86
|
+
def shiplog_match_length(url)
|
|
87
|
+
return if url.nil?
|
|
88
|
+
|
|
89
|
+
uri = URI.parse(url)
|
|
90
|
+
return if uri.scheme && !(uri.scheme.match?(/\Ahttps?\z/i) && uri.host.to_s.casecmp?(request.host))
|
|
91
|
+
|
|
92
|
+
prefix = (uri.path || "").chomp("/")
|
|
93
|
+
current = request.path.to_s.chomp("/")
|
|
94
|
+
return (prefix.length if current == prefix) if prefix.empty?
|
|
95
|
+
|
|
96
|
+
prefix.length if current == prefix || current.start_with?("#{prefix}/")
|
|
97
|
+
rescue URI::InvalidURIError
|
|
98
|
+
nil
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
module Shiplog
|
|
2
|
+
# The "what's new" count for host views: `helper Shiplog::UnseenHelper`, then
|
|
3
|
+
# `shiplog_unseen_count(current_user.changelog_seen_at)`. Shiplog doesn't know about users, so the host
|
|
4
|
+
# supplies the "last seen" time (and stamps it from config.on_changelog_seen).
|
|
5
|
+
module UnseenHelper
|
|
6
|
+
# The number of approved features and changes approved after `since` (all of them while it is nil), at most 99.
|
|
7
|
+
def shiplog_unseen_count(since) = Shiplog.unseen_count(since:)
|
|
8
|
+
end
|
|
9
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
module Shiplog
|
|
2
|
+
# Backfills product areas for entries that have none (see AreaDetector). Enqueued by the admin's Areas page.
|
|
3
|
+
# A failure is logged and recorded for the Areas page (Setting "area_detection"), never raised: the job is not
|
|
4
|
+
# retried, the admin clicks again once the cause (usually the rewriter) is fixed.
|
|
5
|
+
class DetectAreasJob < ApplicationJob
|
|
6
|
+
SETTING = "area_detection".freeze
|
|
7
|
+
MAX_ERROR_CHARS = 500
|
|
8
|
+
|
|
9
|
+
# recheck: true first makes the entries the AI left unplaced eligible again.
|
|
10
|
+
def perform(recheck: false)
|
|
11
|
+
assigned = AreaDetector.new.call(recheck:)
|
|
12
|
+
Setting.write(SETTING, at: Time.current.iso8601, assigned:) if assigned # nil: another run was in progress
|
|
13
|
+
rescue StandardError => error
|
|
14
|
+
Rails.logger.error("[shiplog] Detect areas failed: #{error.class}: #{error.message}\n#{error.backtrace&.first(5)&.join("\n")}")
|
|
15
|
+
Setting.write(SETTING, at: Time.current.iso8601, error: "#{error.class}: #{error.message}".truncate(MAX_ERROR_CHARS))
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -15,6 +15,7 @@ module Shiplog
|
|
|
15
15
|
when "pull_request" then import(repository, number, Github::Queries::PULL_REQUEST, "pullRequest", Sync::PullRequestImporter)
|
|
16
16
|
else raise ArgumentError, "Unknown kind: #{kind.inspect}"
|
|
17
17
|
end
|
|
18
|
+
Sync::UpgradeDetector.call_isolated(repository) if kind.to_s == "pull_request"
|
|
18
19
|
Drafter.call
|
|
19
20
|
end
|
|
20
21
|
|
|
@@ -2,8 +2,11 @@ module Shiplog
|
|
|
2
2
|
class RewriteEntryJob < ApplicationJob
|
|
3
3
|
discard_on ActiveRecord::RecordNotFound
|
|
4
4
|
|
|
5
|
+
# After writing the draft, screenshots from the GitHub body are attached to a draft that has none (the
|
|
6
|
+
# fetcher checks, never raises, and leaves approved entries alone).
|
|
5
7
|
def perform(entry_id, force: false)
|
|
6
|
-
Rewriter.new(Entry.find(entry_id)).call(force:)
|
|
8
|
+
entry = Rewriter.new(Entry.find(entry_id)).call(force:)
|
|
9
|
+
ImageFetcher.call(entry)
|
|
7
10
|
end
|
|
8
11
|
end
|
|
9
12
|
end
|
|
@@ -2,9 +2,13 @@ module Shiplog
|
|
|
2
2
|
class ScanChangesJob < ApplicationJob
|
|
3
3
|
DEFAULT_WINDOW = 7.days
|
|
4
4
|
|
|
5
|
+
# Where a repository's next scan starts, and so the oldest merge nothing will draft before: the end of the
|
|
6
|
+
# last scan, or DEFAULT_WINDOW ago for a repository never scanned.
|
|
7
|
+
def self.window_start(repository) = repository.changes_scanned_at || DEFAULT_WINDOW.ago
|
|
8
|
+
|
|
5
9
|
def perform
|
|
6
10
|
client = Github::Client.new
|
|
7
|
-
Shiplog.config.
|
|
11
|
+
Shiplog.config.repository_names.each do |full_name|
|
|
8
12
|
scan(Repository.find_or_create_by!(full_name:), client)
|
|
9
13
|
end
|
|
10
14
|
Drafter.call
|
|
@@ -12,19 +16,29 @@ module Shiplog
|
|
|
12
16
|
|
|
13
17
|
private
|
|
14
18
|
def scan(repository, client)
|
|
15
|
-
since =
|
|
19
|
+
since = self.class.window_start(repository)
|
|
16
20
|
started_at = Time.current
|
|
17
21
|
# Import late-synced PRs first so anything merged before `started_at` is visible to the scan.
|
|
18
22
|
Sync::RepositorySync.new(repository, client:).call
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
23
|
+
if Shiplog.config.changelog_repository?(repository.full_name)
|
|
24
|
+
Sync::CommitScanner.new(repository, client:).call(since:)
|
|
25
|
+
draft_changes(ChangeCandidates.new(repository:, since:, before: started_at).to_a)
|
|
26
|
+
end
|
|
27
|
+
repository.update!(changes_scanned_at: window_end(repository, started_at, since))
|
|
22
28
|
rescue Github::RateLimited
|
|
23
29
|
raise
|
|
24
30
|
rescue StandardError => error
|
|
25
31
|
Rails.error.report(error, handled: true, context: { repository: repository.full_name })
|
|
26
32
|
end
|
|
27
33
|
|
|
34
|
+
# Pull requests of this window that await upgrade detection are held back by ChangeCandidates, so the window
|
|
35
|
+
# stays at the oldest one and the next scan reconsiders it once it is checked. It never ends before `since`:
|
|
36
|
+
# older pending pull requests are outside the window, and the window must not move backwards.
|
|
37
|
+
def window_end(repository, started_at, since)
|
|
38
|
+
oldest_pending = Shiplog.config.changelog_repository?(repository.full_name) ? ChangeCandidates.oldest_pending(repository, since:) : nil
|
|
39
|
+
[ [ started_at, oldest_pending ].compact.min, since ].max
|
|
40
|
+
end
|
|
41
|
+
|
|
28
42
|
def draft_changes(candidates)
|
|
29
43
|
candidates.each { |subject| Drafter.draft(subject, "change", released_on: released_on(subject)) }
|
|
30
44
|
end
|
|
@@ -2,7 +2,7 @@ module Shiplog
|
|
|
2
2
|
class SyncJob < ApplicationJob
|
|
3
3
|
def perform
|
|
4
4
|
client = Github::Client.new
|
|
5
|
-
repositories = Shiplog.config.
|
|
5
|
+
repositories = Shiplog.config.repository_names.map { |full_name| Repository.find_or_create_by!(full_name:) }
|
|
6
6
|
repositories.each { |repository| sync(repository, client) }
|
|
7
7
|
Drafter.call
|
|
8
8
|
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
module Shiplog
|
|
2
|
+
# Translates an area's name into every locale of config.locales with one LLM call (see AreaTranslator).
|
|
3
|
+
# Enqueued when an area is created or renamed, by "Translate names" (with `force: true`) and by
|
|
4
|
+
# `shiplog:translate_approved`. Without force a locale that has a name is left alone, an admin's edit included;
|
|
5
|
+
# a repeat when every locale has one makes no LLM call.
|
|
6
|
+
#
|
|
7
|
+
# A failure (the rewriter raised, or its reply was unusable) is logged and retried twice, with a growing wait,
|
|
8
|
+
# then logged again and dropped: never an endless retry. The English name stays shown meanwhile.
|
|
9
|
+
class TranslateAreaJob < ApplicationJob
|
|
10
|
+
ATTEMPTS = 3
|
|
11
|
+
|
|
12
|
+
retry_on StandardError, attempts: ATTEMPTS, wait: :polynomially_longer do |job, error|
|
|
13
|
+
Rails.logger.error("[shiplog] Giving up translating area #{job.arguments.first.try(:id)} after #{ATTEMPTS} attempts: #{error.class}: #{error.message}")
|
|
14
|
+
end
|
|
15
|
+
# Declared after retry_on, which makes it win: an area deleted since the job was enqueued is not an error.
|
|
16
|
+
discard_on ActiveJob::DeserializationError, ActiveRecord::RecordNotFound
|
|
17
|
+
|
|
18
|
+
def perform(area, force: false)
|
|
19
|
+
AreaTranslator.new(area).call(force:)
|
|
20
|
+
rescue StandardError => error
|
|
21
|
+
Rails.logger.error("[shiplog] Translating area #{area.id} failed (attempt #{executions} of #{ATTEMPTS}): #{error.class}: #{error.message}")
|
|
22
|
+
raise
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|