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.
Files changed (96) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +97 -0
  3. data/README.md +266 -9
  4. data/app/controllers/shiplog/admin/area_detections_controller.rb +15 -0
  5. data/app/controllers/shiplog/admin/area_translations_controller.rb +19 -0
  6. data/app/controllers/shiplog/admin/areas/merges_controller.rb +19 -0
  7. data/app/controllers/shiplog/admin/areas/moves_controller.rb +13 -0
  8. data/app/controllers/shiplog/admin/areas_controller.rb +48 -0
  9. data/app/controllers/shiplog/admin/entries/images/moves_controller.rb +20 -0
  10. data/app/controllers/shiplog/admin/entries/images_controller.rb +76 -0
  11. data/app/controllers/shiplog/admin/entries/translations/retranslations_controller.rb +23 -0
  12. data/app/controllers/shiplog/admin/entries/translations_controller.rb +35 -0
  13. data/app/controllers/shiplog/admin/entries_controller.rb +9 -3
  14. data/app/controllers/shiplog/admin/syncs_controller.rb +3 -2
  15. data/app/controllers/shiplog/changelog_entries_controller.rb +26 -0
  16. data/app/controllers/shiplog/changelog_feeds_controller.rb +19 -0
  17. data/app/controllers/shiplog/changelogs_controller.rb +30 -4
  18. data/app/controllers/shiplog/public_controller.rb +58 -1
  19. data/app/controllers/shiplog/roadmaps_controller.rb +27 -2
  20. data/app/frontend/shiplog/admin.css +9 -0
  21. data/app/frontend/shiplog/shiplog.css +168 -8
  22. data/app/helpers/shiplog/admin/entries_helper.rb +15 -0
  23. data/app/helpers/shiplog/callable_helper.rb +12 -0
  24. data/app/helpers/shiplog/frontend_helper.rb +1 -0
  25. data/app/helpers/shiplog/permalink_helper.rb +113 -0
  26. data/app/helpers/shiplog/roadmap_helper.rb +24 -0
  27. data/app/helpers/shiplog/switcher_helper.rb +101 -0
  28. data/app/helpers/shiplog/unseen_helper.rb +9 -0
  29. data/app/jobs/shiplog/detect_areas_job.rb +18 -0
  30. data/app/jobs/shiplog/refresh_job.rb +1 -0
  31. data/app/jobs/shiplog/rewrite_entry_job.rb +4 -1
  32. data/app/jobs/shiplog/scan_changes_job.rb +19 -5
  33. data/app/jobs/shiplog/sync_job.rb +1 -1
  34. data/app/jobs/shiplog/translate_area_job.rb +25 -0
  35. data/app/jobs/shiplog/translate_entry_job.rb +25 -0
  36. data/app/models/shiplog/area.rb +156 -0
  37. data/app/models/shiplog/area_detector.rb +146 -0
  38. data/app/models/shiplog/area_translator.rb +77 -0
  39. data/app/models/shiplog/change_candidates.rb +39 -4
  40. data/app/models/shiplog/entry/images.rb +127 -0
  41. data/app/models/shiplog/entry.rb +218 -2
  42. data/app/models/shiplog/entry_translation.rb +15 -0
  43. data/app/models/shiplog/image_fetcher/downloader.rb +98 -0
  44. data/app/models/shiplog/image_fetcher/sources.rb +41 -0
  45. data/app/models/shiplog/image_fetcher.rb +72 -0
  46. data/app/models/shiplog/image_type.rb +111 -0
  47. data/app/models/shiplog/pull_request.rb +4 -0
  48. data/app/models/shiplog/pull_request_upgrade.rb +5 -0
  49. data/app/models/shiplog/rewriter/prompt.rb +57 -9
  50. data/app/models/shiplog/rewriter.rb +50 -13
  51. data/app/models/shiplog/setting.rb +19 -0
  52. data/app/models/shiplog/sync/pull_request_importer.rb +23 -1
  53. data/app/models/shiplog/sync/repository_sync.rb +2 -0
  54. data/app/models/shiplog/sync/upgrade_detector.rb +203 -0
  55. data/app/models/shiplog/translator/prompt.rb +40 -0
  56. data/app/models/shiplog/translator.rb +99 -0
  57. data/app/views/layouts/shiplog/application.html.erb +4 -0
  58. data/app/views/shiplog/_empty.html.erb +1 -1
  59. data/app/views/shiplog/_page_header.html.erb +2 -1
  60. data/app/views/shiplog/_pagination.html.erb +3 -3
  61. data/app/views/shiplog/_switcher.html.erb +13 -0
  62. data/app/views/shiplog/admin/_nav.html.erb +2 -1
  63. data/app/views/shiplog/admin/areas/index.html.erb +78 -0
  64. data/app/views/shiplog/admin/entries/_entry_row.html.erb +4 -0
  65. data/app/views/shiplog/admin/entries/_images.html.erb +41 -0
  66. data/app/views/shiplog/admin/entries/_translations.html.erb +42 -0
  67. data/app/views/shiplog/admin/entries/edit.html.erb +12 -0
  68. data/app/views/shiplog/changelog_entries/show.html.erb +25 -0
  69. data/app/views/shiplog/changelog_feeds/show.atom.builder +24 -0
  70. data/app/views/shiplog/changelogs/_areas.html.erb +8 -0
  71. data/app/views/shiplog/changelogs/_entry.html.erb +2 -5
  72. data/app/views/shiplog/changelogs/_entry_content.html.erb +10 -0
  73. data/app/views/shiplog/changelogs/_search.html.erb +18 -0
  74. data/app/views/shiplog/changelogs/show.html.erb +20 -5
  75. data/app/views/shiplog/roadmaps/_card.html.erb +14 -3
  76. data/app/views/shiplog/roadmaps/_column.html.erb +4 -4
  77. data/app/views/shiplog/roadmaps/_shipped.html.erb +15 -0
  78. data/app/views/shiplog/roadmaps/show.html.erb +11 -2
  79. data/config/admin_routes.rb +16 -0
  80. data/config/locales/en.yml +105 -0
  81. data/config/locales/es.yml +223 -0
  82. data/config/routes.rb +5 -0
  83. data/db/migrate/20261027000001_create_shiplog_pull_request_upgrades.rb +30 -0
  84. data/db/migrate/20261027000002_shiplog_0_3_0.rb +105 -0
  85. data/lib/generators/shiplog/install/install_generator.rb +29 -1
  86. data/lib/generators/shiplog/install/templates/initializer.rb.tt +82 -1
  87. data/lib/shiplog/classes.rb +29 -1
  88. data/lib/shiplog/configuration.rb +245 -3
  89. data/lib/shiplog/engine.rb +3 -0
  90. data/lib/shiplog/github/client.rb +42 -9
  91. data/lib/shiplog/github/errors.rb +3 -0
  92. data/lib/shiplog/github/queries.rb +44 -1
  93. data/lib/shiplog/version.rb +1 -1
  94. data/lib/shiplog.rb +73 -0
  95. data/lib/tasks/shiplog.rake +29 -0
  96. metadata +48 -1
@@ -19,6 +19,18 @@ en:
19
19
  roadmap:
20
20
  title: Roadmap
21
21
  lede: What we're working on and what comes next.
22
+ parts:
23
+ one: Includes %{count} part
24
+ other: Includes %{count} parts
25
+ shipped_heading: Recently shipped
26
+ shipped_date_format: "%-d %B"
27
+ full_changelog: See the full changelog
28
+ footnote: ""
29
+ suggest: Suggest a feature
30
+ switcher:
31
+ label: Product updates
32
+ roadmap: Roadmap
33
+ changelog: Changelog
22
34
  changelog:
23
35
  title: Changelog
24
36
  lede: New features and improvements, as they ship.
@@ -26,12 +38,69 @@ en:
26
38
  label: Pagination
27
39
  older: Older
28
40
  newer: Newer
41
+ search:
42
+ label: Search the changelog
43
+ submit: Search
44
+ summary:
45
+ one: "%{count} result for “%{query}”"
46
+ other: "%{count} results for “%{query}”"
47
+ clear: Clear search
48
+ empty: "No entries match “%{query}”."
49
+ areas:
50
+ label: Filter by area
51
+ all: All
52
+ permalink:
53
+ title: "%{title} · %{changelog}"
54
+ back: "← Back to the changelog"
55
+ nav_label: More changelog entries
56
+ newer: "← Newer: %{title}"
57
+ older: "Older: %{title} →"
58
+ feed:
59
+ title_with_product: "%{product} changelog"
60
+ link_title: Changelog (Atom)
61
+ subscribe: Subscribe (Atom)
29
62
  admin:
30
63
  title: Shiplog admin
31
64
  nav:
32
65
  drafts: Drafts
33
66
  board: Roadmap board
34
67
  sync: Sync
68
+ areas: Areas
69
+ areas:
70
+ title: Product areas
71
+ help: The AI groups entries into areas (at most %{max}) while it rewrites them. Visitors can filter the changelog by area once two areas have published entries.
72
+ detect: Detect areas
73
+ recheck: Re-check unplaced entries
74
+ recheck_help: Ask the AI again about entries it left without an area, e.g. after renaming, merging or adding areas.
75
+ no_rewriter: Detection needs config.rewriter, which is not set.
76
+ last_detection: "Last detection: %{time}, assigned %{count}"
77
+ last_error: "Last detection failed (%{time}): %{error}"
78
+ detection_queued: Detection queued. Refresh in a moment.
79
+ translate_names: Translate names
80
+ translate_names_help: Ask the AI for the names that are missing in some language. Names that are already there, including ones you edited, are kept.
81
+ no_locales: Translating names needs config.locales, which is empty.
82
+ no_rewriter_translate: Translating names needs config.rewriter, which is not set.
83
+ translation_queued:
84
+ one: Translation queued for %{count} area. Refresh in a moment.
85
+ other: Translation queued for %{count} areas. Refresh in a moment.
86
+ empty: No areas yet. Detect areas to propose some from your entries.
87
+ columns:
88
+ name: Name
89
+ entries: Entries
90
+ order: Order
91
+ merge: Merge into
92
+ name_label: Name
93
+ rename: Rename
94
+ renamed: Area renamed.
95
+ move_up: Move up
96
+ move_down: Move down
97
+ merge: Merge
98
+ merge_into: Merge into
99
+ merged: "%{from} merged into %{into}."
100
+ merge_target: Choose another area to merge into.
101
+ merge_help: Merging moves the entries to the other area and deletes this one. Deleting an area leaves its entries without one.
102
+ delete: Delete
103
+ deleted: Area deleted. Its entries have no area now.
35
104
  entries:
36
105
  title: Entries
37
106
  filters: Filter entries
@@ -53,6 +122,8 @@ en:
53
122
  restore: Restore
54
123
  save: Save
55
124
  regenerate: Regenerate
125
+ no_area: None
126
+ area_gone: no longer exists
56
127
  regenerate_warning: Regenerating unpublishes this entry until re-approved.
57
128
  source_changed: GitHub text changed since this draft
58
129
  rewrite_error: Rewrite failed
@@ -60,6 +131,39 @@ en:
60
131
  public_text: Public text
61
132
  source: GitHub source
62
133
  back: Back to drafts
134
+ images:
135
+ title: Screenshots
136
+ help: Shown under the text on the changelog (the first one in the list, all of them on the entry's page). Taken from the GitHub issue or pull request when the draft is written; you can also upload your own.
137
+ empty: No screenshots yet.
138
+ alt: Alt text
139
+ alt_placeholder: Describe the image (defaults to the title)
140
+ save_alt: Save alt text
141
+ remove: Remove
142
+ move_up: Up
143
+ move_down: Down
144
+ upload: Upload images
145
+ upload_label: Images (PNG, JPEG, GIF or WebP, up to 5 MB each)
146
+ none_selected: Choose at least one image.
147
+ rejected: "Not added, only PNG, JPEG, GIF and WebP images up to 5 MB and 10,000 pixels a side are accepted: %{names}"
148
+ added:
149
+ one: Image added.
150
+ other: "%{count} images added."
151
+ alt_saved: Alt text saved.
152
+ removed: Image removed.
153
+ moved: Image moved.
154
+ translations:
155
+ title: Translations
156
+ help: "Approving an entry translates it automatically, and the translation is published with the English text. A translation you edit is kept until the English text changes; then it is translated again. A stale or missing translation is not shown: visitors see the English text."
157
+ not_approved: Translations are written when the entry is approved.
158
+ missing: Missing
159
+ stale: Stale
160
+ current: Up to date
161
+ edited: Edited by an admin
162
+ retranslate: Retranslate
163
+ saved: Translation saved.
164
+ queued: Translation queued. Refresh in a moment.
165
+ only_approved: Only approved entries are translated.
166
+ no_rewriter: Translating needs config.rewriter, which is not set.
63
167
  category_options:
64
168
  feature: New feature
65
169
  change: Improvement
@@ -81,6 +185,7 @@ en:
81
185
  body: Text
82
186
  released_on: Released on
83
187
  category: Category
188
+ area: Area
84
189
  reason: Reason
85
190
  board:
86
191
  title: Roadmap board
@@ -0,0 +1,223 @@
1
+ # Textos de Shiplog en español. Los hosts pueden sobrescribir cualquier clave en sus propios archivos de idioma.
2
+ es:
3
+ activerecord:
4
+ attributes:
5
+ shiplog/entry:
6
+ changelog_kind: Categoría
7
+ # Los nombres de los meses que usa el formato de fecha de Shiplog (rails-i18n los trae también; son los mismos).
8
+ date:
9
+ month_names: [~, enero, febrero, marzo, abril, mayo, junio, julio, agosto, septiembre, octubre, noviembre, diciembre]
10
+ shiplog:
11
+ columns:
12
+ in_progress: En desarrollo
13
+ coming_soon: Próximamente
14
+ later: Más adelante
15
+ in_progress_description: Lo que estamos construyendo ahora
16
+ coming_soon_description: Lo siguiente en la lista
17
+ later_description: En nuestro radar
18
+ categories:
19
+ feature: Nuevo
20
+ change: Mejorado
21
+ empty: Todavía no hay nada
22
+ date_format: "%-d de %B de %Y"
23
+ roadmap:
24
+ title: Hoja de ruta
25
+ lede: En qué estamos trabajando y qué viene después.
26
+ parts:
27
+ one: Incluye %{count} parte
28
+ other: Incluye %{count} partes
29
+ shipped_heading: Lanzado recientemente
30
+ shipped_date_format: "%-d de %B"
31
+ full_changelog: Ver el registro de cambios completo
32
+ footnote: ""
33
+ suggest: Sugerir una función
34
+ switcher:
35
+ label: Actualizaciones del producto
36
+ roadmap: Hoja de ruta
37
+ changelog: Registro de cambios
38
+ changelog:
39
+ title: Registro de cambios
40
+ lede: Nuevas funciones y mejoras, a medida que se lanzan.
41
+ pagination:
42
+ label: Paginación
43
+ older: Anteriores
44
+ newer: Más recientes
45
+ search:
46
+ label: Buscar en el registro de cambios
47
+ submit: Buscar
48
+ summary:
49
+ one: "%{count} resultado para “%{query}”"
50
+ other: "%{count} resultados para “%{query}”"
51
+ clear: Borrar búsqueda
52
+ empty: "Ninguna entrada coincide con “%{query}”."
53
+ areas:
54
+ label: Filtrar por área
55
+ all: Todas
56
+ permalink:
57
+ title: "%{title} · %{changelog}"
58
+ back: "← Volver al registro de cambios"
59
+ nav_label: Más cambios
60
+ newer: "← Más reciente: %{title}"
61
+ older: "Anterior: %{title} →"
62
+ feed:
63
+ title_with_product: "Registro de cambios de %{product}"
64
+ link_title: Registro de cambios (Atom)
65
+ subscribe: Suscribirse (Atom)
66
+ admin:
67
+ title: Administración de Shiplog
68
+ nav:
69
+ drafts: Borradores
70
+ board: Tablero de la hoja de ruta
71
+ sync: Sincronización
72
+ areas: Áreas
73
+ areas:
74
+ title: Áreas del producto
75
+ help: La IA agrupa las entradas en áreas (como máximo %{max}) mientras las reescribe. Los visitantes pueden filtrar el registro de cambios por área cuando hay dos áreas con entradas publicadas.
76
+ detect: Detectar áreas
77
+ recheck: Revisar de nuevo las entradas sin área
78
+ recheck_help: Vuelve a preguntar a la IA por las entradas que dejó sin área, por ejemplo después de renombrar, fusionar o añadir áreas.
79
+ no_rewriter: La detección necesita config.rewriter, que no está configurado.
80
+ last_detection: "Última detección: %{time}, %{count} asignadas"
81
+ last_error: "La última detección falló (%{time}): %{error}"
82
+ detection_queued: Detección en cola. Actualiza en un momento.
83
+ translate_names: Traducir nombres
84
+ translate_names_help: Pide a la IA los nombres que faltan en algún idioma. Los nombres que ya existen, incluidos los que editaste, se conservan.
85
+ no_locales: Traducir los nombres necesita config.locales, que está vacío.
86
+ no_rewriter_translate: Traducir los nombres necesita config.rewriter, que no está configurado.
87
+ translation_queued:
88
+ one: Traducción en cola para %{count} área. Actualiza en un momento.
89
+ other: Traducción en cola para %{count} áreas. Actualiza en un momento.
90
+ empty: Todavía no hay áreas. Detecta áreas para que se propongan a partir de tus entradas.
91
+ columns:
92
+ name: Nombre
93
+ entries: Entradas
94
+ order: Orden
95
+ merge: Fusionar con
96
+ name_label: Nombre
97
+ rename: Renombrar
98
+ renamed: Área renombrada.
99
+ move_up: Subir
100
+ move_down: Bajar
101
+ merge: Fusionar
102
+ merge_into: Fusionar con
103
+ merged: "%{from} se fusionó con %{into}."
104
+ merge_target: Elige otra área con la que fusionar.
105
+ merge_help: Al fusionar, las entradas pasan a la otra área y esta se elimina. Si eliminas un área, sus entradas se quedan sin área.
106
+ delete: Eliminar
107
+ deleted: Área eliminada. Sus entradas ya no tienen área.
108
+ entries:
109
+ title: Entradas
110
+ filters: Filtrar entradas
111
+ all_kinds: Todos los tipos
112
+ empty: Ninguna entrada coincide.
113
+ pagination: Paginación
114
+ older: Anteriores
115
+ newer: Más recientes
116
+ saved: Entrada guardada.
117
+ approved: Entrada aprobada.
118
+ discarded: Entrada descartada.
119
+ restored: Entrada devuelta a borrador.
120
+ invalid_transition: Esa acción no está disponible para esta entrada en su estado actual.
121
+ rewrite_queued: Reescritura en cola. Actualiza en un momento.
122
+ edit: Editar
123
+ edit_title: "Editar %{kind}"
124
+ approve: Aprobar
125
+ discard: Descartar
126
+ restore: Restaurar
127
+ save: Guardar
128
+ regenerate: Regenerar
129
+ no_area: Ninguna
130
+ area_gone: ya no existe
131
+ regenerate_warning: Al regenerar, esta entrada deja de estar publicada hasta que se apruebe de nuevo.
132
+ source_changed: El texto de GitHub cambió desde este borrador
133
+ rewrite_error: Falló la reescritura
134
+ not_rewritten: Todavía sin reescribir
135
+ public_text: Texto público
136
+ source: Origen en GitHub
137
+ back: Volver a los borradores
138
+ images:
139
+ title: Capturas de pantalla
140
+ help: Se muestran bajo el texto en el registro de cambios (la primera en la lista, todas en la página de la entrada). Se toman de la incidencia o del pull request de GitHub al escribir el borrador; también puedes subir las tuyas.
141
+ empty: Todavía no hay capturas.
142
+ alt: Texto alternativo
143
+ alt_placeholder: Describe la imagen (por defecto, el título)
144
+ save_alt: Guardar texto alternativo
145
+ remove: Quitar
146
+ move_up: Subir
147
+ move_down: Bajar
148
+ upload: Subir imágenes
149
+ upload_label: Imágenes (PNG, JPEG, GIF o WebP, hasta 5 MB cada una)
150
+ none_selected: Elige al menos una imagen.
151
+ rejected: "No se añadieron; solo se aceptan imágenes PNG, JPEG, GIF y WebP de hasta 5 MB y 10.000 píxeles por lado: %{names}"
152
+ added:
153
+ one: Imagen añadida.
154
+ other: "%{count} imágenes añadidas."
155
+ alt_saved: Texto alternativo guardado.
156
+ removed: Imagen quitada.
157
+ moved: Imagen movida.
158
+ translations:
159
+ title: Traducciones
160
+ help: "Al aprobar una entrada, esta se traduce automáticamente, y la traducción se publica junto con el texto en inglés. Una traducción que edites se conserva hasta que cambie el texto en inglés; entonces se vuelve a traducir. Una traducción desactualizada o ausente no se muestra: los visitantes ven el texto en inglés."
161
+ not_approved: Las traducciones se escriben cuando se aprueba la entrada.
162
+ missing: Falta
163
+ stale: Desactualizada
164
+ current: Al día
165
+ edited: Editada por un administrador
166
+ retranslate: Traducir de nuevo
167
+ saved: Traducción guardada.
168
+ queued: Traducción en cola. Actualiza en un momento.
169
+ only_approved: Solo se traducen las entradas aprobadas.
170
+ no_rewriter: Para traducir hace falta config.rewriter, que no está configurado.
171
+ category_options:
172
+ feature: Nueva función
173
+ change: Mejora
174
+ statuses:
175
+ draft: Borradores
176
+ approved: Aprobadas
177
+ discarded: Descartadas
178
+ kinds:
179
+ roadmap: Hoja de ruta
180
+ feature: Función
181
+ change: Cambio
182
+ columns:
183
+ entry: Entrada
184
+ kind: Tipo
185
+ source: Origen
186
+ actions: Acciones
187
+ fields:
188
+ title: Título
189
+ body: Texto
190
+ released_on: Fecha de lanzamiento
191
+ category: Categoría
192
+ area: Área
193
+ reason: Motivo
194
+ board:
195
+ title: Tablero de la hoja de ruta
196
+ help: Arrastra las tarjetas entre columnas para fijarlas y ordenarlas. Sin JavaScript, usa el botón Mover de cada tarjeta.
197
+ updated: Tablero actualizado.
198
+ not_approved: Sin aprobar
199
+ pinned: Fijada
200
+ move_to: Mover a
201
+ move: Mover
202
+ unpin: Quitar de fijadas
203
+ sync:
204
+ title: Estado de la sincronización
205
+ queued: Sincronización en cola.
206
+ webhook_secret_missing: El secreto del webhook no está configurado, así que se rechazan los webhooks de GitHub.
207
+ github_token_missing: El token de GitHub no está configurado, así que no se puede sincronizar.
208
+ sync_now: Sincronizar ahora
209
+ sync_and_scan: Sincronizar y buscar cambios
210
+ repositories: Repositorios
211
+ no_repositories: No hay repositorios configurados.
212
+ repository: Repositorio
213
+ synced_at: Última sincronización
214
+ changes_scanned_at: Cambios revisados
215
+ last_error: Último error
216
+ never: Nunca
217
+ deliveries: Últimas entregas de webhook
218
+ no_deliveries: Todavía no hay entregas.
219
+ event: Evento
220
+ action: Acción
221
+ received_at: Recibida
222
+ processed_at: Procesada
223
+ error: Error
data/config/routes.rb CHANGED
@@ -1,5 +1,10 @@
1
1
  Shiplog::Engine.routes.draw do
2
2
  get Shiplog.config.roadmap_path, to: "roadmaps#show", as: :roadmap
3
+ # The feed and the permalinks come before the plain changelog route, whose optional (.:format) would
4
+ # otherwise take /changelog.atom. A slug has no dot, and the permalink takes no format.
5
+ get "#{Shiplog.config.changelog_path}.atom", to: "changelog_feeds#show", as: :changelog_feed, defaults: { format: "atom" }
6
+ get "#{Shiplog.config.changelog_path}/:slug", to: "changelog_entries#show", as: :changelog_entry,
7
+ format: false, constraints: { slug: /[a-z0-9-]+/ }
3
8
  get Shiplog.config.changelog_path, to: "changelogs#show", as: :changelog
4
9
  post "shiplog/github/webhook", to: "webhooks#create", as: :github_webhook
5
10
  get "shiplog/assets/:version/:name", to: "frontends#show", as: :frontend, constraints: { version: /[^\/]+/, name: /[a-z_]+\.(css|js)/ }
@@ -0,0 +1,30 @@
1
+ class CreateShiplogPullRequestUpgrades < ActiveRecord::Migration[8.1]
2
+ def change
3
+ # What upgrade detection needs from the pull request, and its "checked" marker: a pull request with
4
+ # no tracked upgrade has no upgrade row, so only a column can say "inspected, nothing found".
5
+ change_table :shiplog_pull_requests do |t|
6
+ t.string :base_oid
7
+ t.string :merge_oid
8
+ t.boolean :lockfile_changed, null: false, default: false
9
+ t.datetime :upgrades_checked_at
10
+ # Transient failures while inspecting it; the detector gives up (marks it checked) after a few.
11
+ t.integer :upgrade_check_attempts, null: false, default: 0
12
+ end
13
+
14
+ create_table :shiplog_pull_request_upgrades do |t|
15
+ t.references :pull_request, null: false, foreign_key: { to_table: :shiplog_pull_requests }, index: false
16
+ t.string :gem_name, null: false
17
+ t.string :from_ref
18
+ t.string :to_ref
19
+ t.string :gem_repository
20
+ # Not `changes`: that name belongs to ActiveModel::Dirty and Active Record refuses it as an attribute.
21
+ t.jsonb :gem_changes, null: false, default: []
22
+ t.timestamps
23
+ t.index [ :pull_request_id, :gem_name ], unique: true
24
+ end
25
+
26
+ # The changelog and the roadmap's recently shipped list read approved entries newest release first:
27
+ # `status = 'approved' ... ORDER BY released_on DESC`. Equality column first, then the sort column.
28
+ add_index :shiplog_entries, [ :status, :released_on ]
29
+ end
30
+ end
@@ -0,0 +1,105 @@
1
+ # Everything 0.3.0 adds to the schema, one section per feature. Additive and reversible.
2
+ class Shiplog030 < ActiveRecord::Migration[8.1]
3
+ # A migration must not depend on model code that changes later: this is a bare table-backed class.
4
+ class MigrationEntry < ActiveRecord::Base
5
+ self.table_name = "shiplog_entries"
6
+ self.inheritance_column = nil
7
+ end
8
+
9
+ SLUG_MAX_LENGTH = 80
10
+
11
+ def change
12
+ # --- Entry permalinks (/changelog/:slug) ---
13
+ # Set when an entry is first approved, never changed afterwards, so links never break. Nil for roadmap
14
+ # entries and drafts. Entries approved before 0.3.0 get theirs here (oldest first, so the oldest keeps the
15
+ # plain slug on duplicate titles): the public pages never write, so nothing else fills them in.
16
+ add_column :shiplog_entries, :slug, :string
17
+ add_index :shiplog_entries, :slug, unique: true, where: "slug IS NOT NULL"
18
+ reversible do |direction|
19
+ direction.up do
20
+ MigrationEntry.reset_column_information
21
+ backfill_slugs
22
+ end
23
+ end
24
+
25
+ # --- "What's new" unseen count ---
26
+ # `Shiplog.unseen_count` counts approved feature and change entries with `approved_at > since`:
27
+ # an equality column first, then the range column.
28
+ add_index :shiplog_entries, [ :status, :approved_at ]
29
+
30
+ # --- Product areas (detected by AI) ---
31
+ # `entries.area_id` is nullable; deleting an area nullifies it (merging moves the entries first).
32
+ # `area_set_by_admin` marks an area chosen by hand, which detection never overwrites.
33
+ create_table :shiplog_areas do |t|
34
+ t.string :name, null: false
35
+ t.string :slug, null: false
36
+ t.integer :position, null: false, default: 0
37
+ # The name in each of config.locales, keyed by locale string ({ "es" => "Listas de embalaje" }); empty
38
+ # until translated. Slugs stay English, so ?area= URLs are shared across locales.
39
+ t.jsonb :name_translations, null: false, default: {}
40
+ t.timestamps
41
+ end
42
+ add_index :shiplog_areas, :slug, unique: true
43
+ add_reference :shiplog_entries, :area, null: true, index: true, foreign_key: { to_table: :shiplog_areas, on_delete: :nullify }
44
+ add_column :shiplog_entries, :area_set_by_admin, :boolean, null: false, default: false
45
+ # When detection last asked the AI about the entry (placed or not): unplaced entries are not asked again
46
+ # until an admin asks for a re-check.
47
+ add_column :shiplog_entries, :area_detected_at, :datetime
48
+ # A small key-value store for state that belongs to no record, e.g. the result of the last area detection
49
+ # (shown on the admin's Areas page).
50
+ create_table :shiplog_settings do |t|
51
+ t.string :key, null: false
52
+ t.jsonb :value, null: false, default: {}
53
+ t.timestamps
54
+ end
55
+ add_index :shiplog_settings, :key, unique: true
56
+
57
+ # --- Screenshots (needs Active Storage in the host; the columns are harmless without it) ---
58
+ # `image_order` is the ordered list of the entry's image blob ids, `image_alts` the alt text per blob id
59
+ # ({ "12" => "The dark theme" }); a missing alt text falls back to the entry's title.
60
+ add_column :shiplog_entries, :image_order, :jsonb, null: false, default: []
61
+ add_column :shiplog_entries, :image_alts, :jsonb, null: false, default: {}
62
+
63
+ # --- Translations of entries (config.locales) ---
64
+ # One row per entry and locale, published with the English text (no second approval). `source_digest` is the
65
+ # digest of the English title and body it was made from: a row whose digest differs from the entry's current
66
+ # text is stale and never shown. `edited_by_admin` keeps an admin's edit until the English text changes.
67
+ create_table :shiplog_entry_translations do |t|
68
+ t.references :entry, null: false, index: false, foreign_key: { to_table: :shiplog_entries, on_delete: :cascade }
69
+ t.string :locale, null: false
70
+ t.string :title, null: false
71
+ t.text :body, null: false, default: ""
72
+ t.string :source_digest, null: false
73
+ t.boolean :edited_by_admin, null: false, default: false
74
+ t.timestamps
75
+ end
76
+ add_index :shiplog_entry_translations, [ :entry_id, :locale ], unique: true
77
+ end
78
+
79
+ # Gives every approved feature and change with a release date, and no slug, one: the title as [a-z0-9-] only
80
+ # (cut at 80 characters), made unique with -2, -3, ...; entry-<id> when nothing is left. The same rules as
81
+ # Shiplog::Entry#generate_slug at the time of 0.3.0.
82
+ def backfill_slugs
83
+ taken = MigrationEntry.where.not(slug: nil).pluck(:slug).to_set
84
+ MigrationEntry.where(status: "approved", kind: %w[feature change], slug: nil).where.not(released_on: nil)
85
+ .order(:released_on, :approved_at, :id).to_a.each do |entry|
86
+ slug = slug_for(entry, taken)
87
+ taken << slug
88
+ MigrationEntry.where(id: entry.id, slug: nil).update_all(slug:)
89
+ end
90
+ end
91
+
92
+ private
93
+
94
+ def slug_for(entry, taken)
95
+ base = entry.title.to_s.parameterize.tr("_", "-").squeeze("-").first(SLUG_MAX_LENGTH).gsub(/\A-+|-+\z/, "").presence || "entry-#{entry.id}"
96
+ candidate = base
97
+ suffix = 2
98
+ while taken.include?(candidate)
99
+ tail = "-#{suffix}"
100
+ candidate = "#{base.first(SLUG_MAX_LENGTH - tail.length).delete_suffix("-")}#{tail}"
101
+ suffix += 1
102
+ end
103
+ candidate
104
+ end
105
+ end
@@ -17,9 +17,12 @@ module Shiplog
17
17
  page_header: "mb-8",
18
18
  page_title: "text-3xl font-semibold tracking-tight",
19
19
  page_lede: "mt-2 text-gray-600 dark:text-gray-400",
20
+ switcher: "mt-6 inline-flex gap-1 rounded-full bg-gray-100 p-1 text-sm font-medium dark:bg-gray-900",
21
+ switcher_link: "rounded-full px-4 py-1.5 text-gray-600 transition-colors hover:text-gray-900 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 aria-[current=page]:bg-white aria-[current=page]:text-gray-900 aria-[current=page]:shadow-sm dark:text-gray-400 dark:hover:text-white dark:aria-[current=page]:bg-gray-700 dark:aria-[current=page]:text-white",
20
22
  columns: "grid gap-6 md:grid-cols-3",
21
23
  column: "rounded-xl border border-gray-200 bg-gray-50 p-4 dark:border-gray-800 dark:bg-gray-900/50",
22
24
  column_header: "mb-4 border-b border-gray-200 pb-3 dark:border-gray-800",
25
+ column_marker: "mr-2 inline-block h-2 w-2 rounded-full bg-gray-400 in-[.shiplog-column--in-progress]:bg-indigo-600 in-[.shiplog-column--coming-soon]:bg-indigo-300",
23
26
  column_title: "inline text-base font-semibold",
24
27
  column_count: "ml-2 rounded-full bg-gray-200 px-2 py-0.5 text-xs font-medium text-gray-700 dark:bg-gray-800 dark:text-gray-300",
25
28
  column_description: "mt-1 text-sm text-gray-500 dark:text-gray-400",
@@ -28,6 +31,16 @@ module Shiplog
28
31
  card: "rounded-lg border border-gray-200 bg-white p-3 shadow-sm transition hover:border-indigo-400 hover:shadow dark:border-gray-800 dark:bg-gray-900 dark:hover:border-indigo-500",
29
32
  card_title: "font-medium",
30
33
  card_body: "mt-1 text-sm text-gray-600 dark:text-gray-400",
34
+ card_meta: "mt-3 flex flex-wrap items-center gap-2 text-xs font-medium text-gray-500 dark:text-gray-400",
35
+ card_tag: "rounded-md bg-gray-100 px-2 py-0.5 text-gray-700 dark:bg-white/10 dark:text-gray-300",
36
+ shipped: "mt-12 grid grid-cols-[1fr_auto] items-baseline gap-x-6 gap-y-4 border-t border-gray-200 pt-8 dark:border-gray-800",
37
+ shipped_title: "text-lg font-semibold",
38
+ shipped_list: "col-span-2 grid gap-6 sm:grid-cols-2 lg:grid-cols-4",
39
+ shipped_item: "flex flex-col gap-1",
40
+ shipped_date: "text-xs tabular-nums text-gray-500 dark:text-gray-400",
41
+ shipped_link: "text-sm font-semibold text-gray-900 hover:text-indigo-600 dark:text-white dark:hover:text-indigo-400",
42
+ footnote: "mt-10 border-t border-gray-200 pt-6 text-center text-sm text-gray-500 dark:border-gray-800 dark:text-gray-400",
43
+ footnote_link: "font-medium text-indigo-600 hover:underline dark:text-indigo-400",
31
44
  months: "space-y-10",
32
45
  month: "border-l-2 border-gray-200 pl-6 dark:border-gray-800",
33
46
  month_title: "mb-4 text-lg font-semibold",
@@ -40,7 +53,22 @@ module Shiplog
40
53
  entry_date: "text-xs text-gray-500 dark:text-gray-400",
41
54
  pagination: "mt-8 flex justify-between",
42
55
  pagination_link: "text-sm font-medium text-indigo-600 hover:underline dark:text-indigo-400",
43
- empty: "text-gray-500 dark:text-gray-400"
56
+ empty: "text-gray-500 dark:text-gray-400",
57
+ search: "my-4 flex gap-2",
58
+ search_input: "min-w-0 flex-1 rounded-md border border-gray-300 bg-white px-3 py-1.5 text-sm dark:border-gray-700 dark:bg-gray-900",
59
+ search_button: "rounded-md bg-indigo-600 px-3 py-1.5 text-sm font-medium text-white hover:bg-indigo-500",
60
+ search_summary: "mb-4 text-sm text-gray-500 dark:text-gray-400",
61
+ search_clear: "ml-2 font-medium text-indigo-600 hover:underline dark:text-indigo-400",
62
+ search_empty: "text-gray-500 dark:text-gray-400",
63
+ permalink: "mx-auto max-w-2xl rounded-lg border border-gray-200 bg-white p-6 dark:border-gray-800 dark:bg-gray-900",
64
+ permalink_back: "mt-6 inline-block text-sm font-medium text-indigo-600 hover:underline dark:text-indigo-400",
65
+ permalink_nav: "mt-8 flex flex-wrap justify-between gap-4 border-t border-gray-200 pt-4 dark:border-gray-800",
66
+ permalink_nav_link: "text-sm font-medium text-indigo-600 hover:underline dark:text-indigo-400",
67
+ areas: "my-4 flex flex-wrap gap-2",
68
+ area_link: "rounded-full border border-gray-300 px-3 py-0.5 text-sm hover:border-indigo-500 dark:border-gray-700 [&.shiplog-areas__link--current]:border-indigo-600 [&.shiplog-areas__link--current]:bg-indigo-600 [&.shiplog-areas__link--current]:text-white",
69
+ entry_area: "text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400",
70
+ entry_images: "mt-3 grid w-full gap-3",
71
+ entry_image: "h-auto max-w-full rounded-lg border border-gray-200 dark:border-gray-800"
44
72
  }.freeze
45
73
 
46
74
  def copy_initializer
@@ -2,6 +2,21 @@ Shiplog.configure do |config|
2
2
  # GitHub repositories to sync ("owner/name"). Sub-issues may cross repositories of the same owner.
3
3
  config.repositories = %w[acme/app acme/app-ios]
4
4
 
5
+ # A repository can also be a Hash with options:
6
+ # changelog: false keep the repository on the roadmap, but never draft changelog entries from its PRs or commits
7
+ # gem: "name" the gem's name in your Gemfile.lock. When a merged PR upgrades it, the changelog gets one
8
+ # entry for the upgrade, built from the gem's changes. Needed because names differ
9
+ # (a "trackberry-produce" gem can live in acme/produce) and a public gem of the same name
10
+ # is not your repository.
11
+ # config.repositories = [
12
+ # "acme/app",
13
+ # { name: "acme/app-ios" },
14
+ # { name: "acme/pricing", changelog: false, gem: "acme-pricing" }
15
+ # ]
16
+
17
+ # Lockfile read in merged pull requests to detect gem upgrades (change it for a monorepo).
18
+ # config.lockfile_path = "Gemfile.lock"
19
+
5
20
  # A String, or a callable evaluated on every GitHub request (e.g. to mint a GitHub App installation token).
6
21
  config.github_token = -> { Rails.application.credentials.dig(:github, :shiplog_token) }
7
22
 
@@ -82,5 +97,71 @@ Shiplog.configure do |config|
82
97
 
83
98
  # Public paths, read when routes are drawn.
84
99
  # config.roadmap_path = "roadmap"
85
- # config.changelog_path = "changelog"
100
+ # config.changelog_path = "changelog" # a-z, 0-9, - and _; "docs/news" nests; outer slashes are stripped
101
+
102
+ # The site's canonical origin (scheme, host, optional port; no path). The Atom feed's ids and links, og:url
103
+ # and the other absolute URLs use it instead of the request's host, which varies (apex vs www, a forged
104
+ # X-Forwarded-Host). Behind a proxy, also set config.assume_ssl = true (or send X-Forwarded-Proto) in Rails.
105
+ # config.canonical_url = "https://example.com"
106
+
107
+ # Pages added to the Roadmap / Changelog switcher. label and path are Strings, or callables evaluated in the
108
+ # view (route helpers, I18n). Render the same switcher on your own pages with `helper Shiplog::SwitcherHelper`
109
+ # and `<%%= shiplog_switcher %>`.
110
+ # config.extra_pages = [ { label: "News", path: -> { main_app.news_path } } ]
111
+
112
+ # Names shown on roadmap cards for the repository an item lives in (Strings or callables).
113
+ # config.repository_labels = { "acme/app-ios" => "iPhone & iPad" }
114
+
115
+ # How many recently shipped changelog entries the roadmap lists; 0 hides the section.
116
+ # config.roadmap_shipped_count = 4
117
+
118
+ # Where the roadmap's "Suggest a feature" link goes (String or callable); no link while nil. Add a footnote
119
+ # next to it with the shiplog.roadmap.footnote translation.
120
+ # config.feedback_path = -> { main_app.new_feedback_path }
121
+
122
+ # The public changelog's search box and ?q= filtering. Set to false to turn search off.
123
+ # config.changelog_search = true
124
+
125
+ # "What's new". Shiplog doesn't know your users, so you supply the "last seen" time. This is called with the
126
+ # controller on every changelog index or permalink view, BEFORE the page renders (a 304 revisit included), so a
127
+ # navbar badge in the same response is current. It may write and runs on every visit: keep it cheap, and write
128
+ # only when something is new. Show `Shiplog.unseen_count(since: user.changelog_seen_at)` (at most 99, counted on
129
+ # approved_at; `helper Shiplog::UnseenHelper` and `shiplog_unseen_count(since)` in views) next to your Changelog
130
+ # link. current_user is often private, hence controller.send (or read a Current attribute).
131
+ # config.on_changelog_seen = lambda do |controller|
132
+ # user = controller.send(:current_user)
133
+ # user.update_column(:changelog_seen_at, Time.current) if user && Shiplog.unseen_count(since: user.changelog_seen_at).positive?
134
+ # end
135
+
136
+ # The 0.3.0 migration gives entries approved so far their permalink slug. The public pages never write (a GET
137
+ # may hit a read replica), so an entry approved by older code during a deploy window has no permalink until
138
+ # `bin/rails shiplog:backfill_slugs` runs. Permalink pages set `content_for :shiplog_og` (Open Graph tags),
139
+ # `content_for :shiplog_description` and `content_for :shiplog_feed` (the Atom discovery link); Shiplog's own
140
+ # layout renders them, so emit them in your layout's <head> when you set `config.public_layout`.
141
+
142
+ # Product areas. The rewriter's reply may name an area (an existing one, or a new name of 1-3 words); Shiplog
143
+ # creates it while fewer than max_areas exist, so the AI cannot sprawl. The changelog offers an area filter
144
+ # (?area=slug) once two areas have published entries. Manage areas in the admin ("Areas"): rename, reorder,
145
+ # merge, delete, and "Detect areas" to backfill entries that have none (it reuses config.rewriter).
146
+ # Entries you set by hand on the edit page are never changed by the AI.
147
+ # config.max_areas = 8
148
+
149
+ # Screenshots in entries. They need Active Storage in your app (require "active_storage/engine", and its tables);
150
+ # without it they stay off and Shiplog logs that once at boot. When a draft is written, the images in the GitHub
151
+ # issue or pull request body are attached to the entry (GitHub image hosts only, 5 MB and PNG, JPEG, GIF or
152
+ # WebP at most; the token needs no extra permission). You can also upload images on the entry edit page.
153
+ # config.images = true
154
+ # config.max_images_per_entry = 4
155
+
156
+ # Translations of entries. Entries are translated into each of these locales with config.rewriter when they are
157
+ # approved, and again when the English title or body of an approved entry changes; the translation is published
158
+ # at once, with no second approval. One LLM call per entry, locale and change. Admins can edit a translation on
159
+ # the entry page (kept until the English text changes) or retranslate it. Pages render the translation when
160
+ # I18n.locale is one of these and it matches the current English text, and English otherwise. Your routes
161
+ # decide which URLs set the locale (e.g. a /es scope). The default locale is dropped from the list.
162
+ # config.locales = [] # e.g. [:es]
163
+
164
+ # Callables above cannot be part of the public pages' ETag. Bump this when the output of a callable you
165
+ # configured changes (a new label, another path) so cached pages are refreshed.
166
+ # config.cache_version = nil
86
167
  end