develoz-rails 0.1.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 +7 -0
- data/.github/workflows/ci.yml +31 -0
- data/.github/workflows/release.yml +459 -0
- data/.rubocop.yml +52 -0
- data/.sisyphus/notepads/develoz-rails-template/learnings.md +855 -0
- data/.tool-versions +2 -0
- data/CHANGELOG.md +28 -0
- data/CONTRIBUTING.md +120 -0
- data/README.md +76 -0
- data/Rakefile +18 -0
- data/config/generators.yml +105 -0
- data/docs/guide/installation.md +574 -0
- data/exe/develoz +6 -0
- data/install +98 -0
- data/lib/develoz/canonical_fetcher.rb +56 -0
- data/lib/develoz/cli.rb +84 -0
- data/lib/develoz/generators/base.rb +134 -0
- data/lib/develoz/manifest.rb +44 -0
- data/lib/develoz/options.rb +55 -0
- data/lib/develoz/version.rb +5 -0
- data/lib/develoz/version_resolver.rb +103 -0
- data/lib/develoz.rb +15 -0
- data/lib/generators/develoz/active_resource/active_resource_generator.rb +25 -0
- data/lib/generators/develoz/active_resource/templates/application_resource.rb.tt +7 -0
- data/lib/generators/develoz/active_resource/templates/example_resource.rb.tt +5 -0
- data/lib/generators/develoz/admin/admin_generator.rb +44 -0
- data/lib/generators/develoz/admin/templates/admin_base_controller.rb.tt +7 -0
- data/lib/generators/develoz/admin/templates/admin_layout.html.erb.tt +38 -0
- data/lib/generators/develoz/admin/templates/dashboard_controller.rb.tt +7 -0
- data/lib/generators/develoz/admin/templates/dashboard_index.html.erb.tt +2 -0
- data/lib/generators/develoz/admin/templates/routes.rb.tt +3 -0
- data/lib/generators/develoz/agents_docs/agents_docs_generator.rb +50 -0
- data/lib/generators/develoz/agents_docs/templates/.github/pull_request_template.md.tt +13 -0
- data/lib/generators/develoz/agents_docs/templates/AGENTS.md.tt +36 -0
- data/lib/generators/develoz/agents_docs/templates/docs/development.md.tt +121 -0
- data/lib/generators/develoz/agents_docs/templates/docs/performance.md.tt +46 -0
- data/lib/generators/develoz/agents_docs/templates/docs/testing.md.tt +92 -0
- data/lib/generators/develoz/agents_docs/templates/spec/cassettes/Example_API/fetches_data_from_an_external_API.yml.tt +23 -0
- data/lib/generators/develoz/agents_docs/templates/spec/requests/example_api_spec.rb.tt +14 -0
- data/lib/generators/develoz/agents_docs/templates/spec/support/faraday.rb.tt +19 -0
- data/lib/generators/develoz/agents_docs/templates/spec/support/vcr.rb.tt +11 -0
- data/lib/generators/develoz/api/api_generator.rb +55 -0
- data/lib/generators/develoz/api/templates/app/blueprints/example_blueprint.rb.tt +6 -0
- data/lib/generators/develoz/api/templates/app/controllers/api/v1/base_controller.rb.tt +96 -0
- data/lib/generators/develoz/api/templates/config/initializers/blueprinter.rb.tt +16 -0
- data/lib/generators/develoz/api/templates/config/initializers/rswag_api.rb.tt +15 -0
- data/lib/generators/develoz/api/templates/config/initializers/rswag_ui.rb.tt +17 -0
- data/lib/generators/develoz/api/templates/spec/requests/api/v1/examples_spec.rb.tt +25 -0
- data/lib/generators/develoz/auth/auth_generator.rb +82 -0
- data/lib/generators/develoz/auth/templates/app/controllers/concerns/authentication.rb.tt +53 -0
- data/lib/generators/develoz/auth/templates/app/controllers/passwords_controller.rb.tt +35 -0
- data/lib/generators/develoz/auth/templates/app/controllers/sessions_controller.rb.tt +22 -0
- data/lib/generators/develoz/auth/templates/app/mailers/passwords_mailer.rb.tt +8 -0
- data/lib/generators/develoz/auth/templates/app/models/current.rb.tt +5 -0
- data/lib/generators/develoz/auth/templates/app/models/user.rb.tt +7 -0
- data/lib/generators/develoz/auth/templates/app/views/passwords/edit.html.erb.tt +9 -0
- data/lib/generators/develoz/auth/templates/app/views/passwords_mailer/reset.html.erb.tt +6 -0
- data/lib/generators/develoz/auth/templates/app/views/sessions/new.html.erb.tt +11 -0
- data/lib/generators/develoz/auth/templates/db/migrate/create_users.rb.tt +13 -0
- data/lib/generators/develoz/auth/templates/spec/requests/passwords_spec.rb.tt +60 -0
- data/lib/generators/develoz/auth/templates/spec/requests/sessions_spec.rb.tt +37 -0
- data/lib/generators/develoz/ci/ci_generator.rb +66 -0
- data/lib/generators/develoz/ci/templates/.github/workflows/ci.yml.tt +18 -0
- data/lib/generators/develoz/ci/templates/.haml-lint.yml.tt +12 -0
- data/lib/generators/develoz/ci/templates/.markdownlint.json.tt +10 -0
- data/lib/generators/develoz/ci/templates/.reek.yml.tt +28 -0
- data/lib/generators/develoz/ci/templates/.rubocop.yml.tt +22 -0
- data/lib/generators/develoz/ci/templates/.stylelintrc.json.tt +4 -0
- data/lib/generators/develoz/ci/templates/.yamllint.tt +14 -0
- data/lib/generators/develoz/ci/templates/Rakefile.tt +73 -0
- data/lib/generators/develoz/ci/templates/bin/ci.tt +6 -0
- data/lib/generators/develoz/ci/templates/biome.json.tt +41 -0
- data/lib/generators/develoz/ci/templates/config/ci.rb.tt +23 -0
- data/lib/generators/develoz/concerns/concerns_generator.rb +48 -0
- data/lib/generators/develoz/concerns/templates/app/models/concerns/configurable.rb.tt +120 -0
- data/lib/generators/develoz/concerns/templates/app/models/concerns/optimized_finders.rb.tt +93 -0
- data/lib/generators/develoz/concerns/templates/app/models/concerns/searchable_concern.rb.tt +31 -0
- data/lib/generators/develoz/concerns/templates/app/models/concerns/transitionable.rb.tt +144 -0
- data/lib/generators/develoz/concerns/templates/app/models/configuration.rb.tt +104 -0
- data/lib/generators/develoz/concerns/templates/db/migrate/add_status_transitions.rb.tt +30 -0
- data/lib/generators/develoz/concerns/templates/db/migrate/create_configurations.rb.tt +25 -0
- data/lib/generators/develoz/concerns/templates/spec/models/concerns/configurable_spec.rb.tt +172 -0
- data/lib/generators/develoz/concerns/templates/spec/models/concerns/optimized_finders_spec.rb.tt +200 -0
- data/lib/generators/develoz/concerns/templates/spec/models/concerns/searchable_concern_spec.rb.tt +84 -0
- data/lib/generators/develoz/concerns/templates/spec/models/concerns/transitionable_spec.rb.tt +234 -0
- data/lib/generators/develoz/concerns/templates/spec/models/configuration_spec.rb.tt +223 -0
- data/lib/generators/develoz/database/database_generator.rb +44 -0
- data/lib/generators/develoz/database/templates/config/database.yml.tt +53 -0
- data/lib/generators/develoz/database/templates/config/initializers/pg_search.rb.tt +5 -0
- data/lib/generators/develoz/database/templates/db/migrate/create_pg_extensions.rb.tt +13 -0
- data/lib/generators/develoz/db_backup/db_backup_generator.rb +47 -0
- data/lib/generators/develoz/db_backup/templates/backup_rake.tt +48 -0
- data/lib/generators/develoz/db_backup/templates/bin_db_backup.tt +25 -0
- data/lib/generators/develoz/db_backup/templates/compose_service.tt +14 -0
- data/lib/generators/develoz/doc_specs/doc_specs_generator.rb +33 -0
- data/lib/generators/develoz/doc_specs/templates/bin/generate-docs.tt +392 -0
- data/lib/generators/develoz/doc_specs/templates/lib/tasks/docs_check.rake.tt +8 -0
- data/lib/generators/develoz/doc_specs/templates/spec/support/doc_screenshot_helper.rb.tt +17 -0
- data/lib/generators/develoz/doc_specs/templates/spec/system/example_doc_spec.rb.tt +38 -0
- data/lib/generators/develoz/docker/docker_generator.rb +54 -0
- data/lib/generators/develoz/docker/templates/bin_dev.tt +5 -0
- data/lib/generators/develoz/docker/templates/bin_docker_entrypoint.tt +8 -0
- data/lib/generators/develoz/docker/templates/bin_run.tt +152 -0
- data/lib/generators/develoz/docker/templates/bin_setup.tt +15 -0
- data/lib/generators/develoz/docker/templates/docker-compose.yml.tt +59 -0
- data/lib/generators/develoz/docker/templates/dockerfile_dev.tt +24 -0
- data/lib/generators/develoz/docs_render/docs_render_generator.rb +46 -0
- data/lib/generators/develoz/docs_render/templates/app/assets/stylesheets/documentation.scss.tt +145 -0
- data/lib/generators/develoz/docs_render/templates/app/controllers/docs_controller.rb.tt +7 -0
- data/lib/generators/develoz/docs_render/templates/app/javascript/docs.js.tt +15 -0
- data/lib/generators/develoz/docs_render/templates/app/models/document.rb.tt +189 -0
- data/lib/generators/develoz/docs_render/templates/app/views/docs/show.html.erb.tt +7 -0
- data/lib/generators/develoz/docs_render/templates/config/initializers/redcarpet_rouge.rb.tt +16 -0
- data/lib/generators/develoz/frontend_core/frontend_core_generator.rb +40 -0
- data/lib/generators/develoz/frontend_core/templates/.annotaterb.yml.tt +16 -0
- data/lib/generators/develoz/frontend_core/templates/config/importmap.rb.tt +12 -0
- data/lib/generators/develoz/frontend_core/templates/config/initializers/pagy.rb.tt +10 -0
- data/lib/generators/develoz/install/install_generator.rb +51 -0
- data/lib/generators/develoz/kamal/kamal_generator.rb +59 -0
- data/lib/generators/develoz/kamal/templates/accessories_postgres.yml.tt +14 -0
- data/lib/generators/develoz/kamal/templates/deploy.yml.tt +43 -0
- data/lib/generators/develoz/kamal/templates/dockerfile_prod.tt +54 -0
- data/lib/generators/develoz/kamal/templates/kamal_secrets.tt +22 -0
- data/lib/generators/develoz/maintenance/maintenance_generator.rb +29 -0
- data/lib/generators/develoz/maintenance/templates/app/tasks/maintenance/example_task.rb.tt +20 -0
- data/lib/generators/develoz/maintenance/templates/lib/tasks/maintenance_counters.rake.tt +27 -0
- data/lib/generators/develoz/push/push_generator.rb +61 -0
- data/lib/generators/develoz/push/templates/app/javascript/pwa/subscription.js.tt +57 -0
- data/lib/generators/develoz/push/templates/app/models/push_subscription.rb.tt +14 -0
- data/lib/generators/develoz/push/templates/app/services/push_notification_service.rb.tt +36 -0
- data/lib/generators/develoz/push/templates/app/views/pwa/sw_push_handlers.js.tt +49 -0
- data/lib/generators/develoz/push/templates/constants_env.tt +5 -0
- data/lib/generators/develoz/push/templates/db/migrate/create_push_subscriptions.rb.tt +15 -0
- data/lib/generators/develoz/pwa/pwa_generator.rb +46 -0
- data/lib/generators/develoz/pwa/templates/app/controllers/pwa_controller.rb.tt +17 -0
- data/lib/generators/develoz/pwa/templates/app/javascript/pwa/registration.js.tt +25 -0
- data/lib/generators/develoz/pwa/templates/app/views/pwa/manifest.json.erb.tt +24 -0
- data/lib/generators/develoz/pwa/templates/app/views/pwa/offline.html.erb.tt +51 -0
- data/lib/generators/develoz/pwa/templates/app/views/pwa/service-worker.js.tt +109 -0
- data/lib/generators/develoz/pwa/templates/routes.rb.tt +3 -0
- data/lib/generators/develoz/solid/solid_generator.rb +52 -0
- data/lib/generators/develoz/solid/templates/app/jobs/application_job.rb.tt +7 -0
- data/lib/generators/develoz/solid/templates/config/cable.yml.tt +10 -0
- data/lib/generators/develoz/solid/templates/config/cache.yml.tt +16 -0
- data/lib/generators/develoz/solid/templates/config/initializers/mission_control.rb.tt +8 -0
- data/lib/generators/develoz/solid/templates/config/initializers/solid.rb.tt +7 -0
- data/lib/generators/develoz/solid/templates/config/queue.yml.tt +18 -0
- data/lib/generators/develoz/solid/templates/config/recurring.yml.tt +20 -0
- data/lib/generators/develoz/strict_loading/strict_loading_generator.rb +17 -0
- data/lib/generators/develoz/strict_loading/templates/config/initializers/strict_loading.rb.tt +13 -0
- data/lib/generators/develoz/testing/templates/rspec.tt +2 -0
- data/lib/generators/develoz/testing/templates/rspec_parallel.tt +2 -0
- data/lib/generators/develoz/testing/templates/spec/rails_helper.rb.tt +32 -0
- data/lib/generators/develoz/testing/templates/spec/spec_helper.rb.tt +31 -0
- data/lib/generators/develoz/testing/testing_generator.rb +39 -0
- data/lib/generators/develoz/tooling/templates/constants.rb.tt +17 -0
- data/lib/generators/develoz/tooling/templates/env.example.tt +4 -0
- data/lib/generators/develoz/tooling/templates/vscode/extensions.json.tt +8 -0
- data/lib/generators/develoz/tooling/templates/vscode/settings.json.tt +26 -0
- data/lib/generators/develoz/tooling/templates/vscode/tasks.json.tt +26 -0
- data/lib/generators/develoz/tooling/tooling_generator.rb +33 -0
- data/lib/generators/develoz/ui/templates/bin/setup_develoz_ui.tt +20 -0
- data/lib/generators/develoz/ui/ui_generator.rb +57 -0
- data/lib/generators/develoz/versioning/templates/app/helpers/application_helper.rb.tt +7 -0
- data/lib/generators/develoz/versioning/templates/app/views/shared/_app_version.html.erb.tt +3 -0
- data/lib/generators/develoz/versioning/templates/spec/helpers/application_helper_spec.rb.tt +11 -0
- data/lib/generators/develoz/versioning/versioning_generator.rb +48 -0
- data/lib/tasks/canonical.rake +27 -0
- data/templates/.keep +0 -0
- data/templates/CANONICAL_SOURCES.md +93 -0
- data/templates/pull_request_template.md.tt +13 -0
- metadata +383 -0
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'fileutils'
|
|
5
|
+
|
|
6
|
+
# Script to generate markdown documentation from system specs with doc comments
|
|
7
|
+
#
|
|
8
|
+
# Usage:
|
|
9
|
+
# bin/generate-docs # Generate docs for all system specs with doc comments
|
|
10
|
+
# bin/generate-docs spec/path/to/file # Generate docs for a specific spec file
|
|
11
|
+
# bin/generate-docs --check # Verify committed docs match their specs (no spec run); exit 1 if stale
|
|
12
|
+
|
|
13
|
+
class DocGenerator
|
|
14
|
+
DOCS_DIR = 'docs/platform-guide'
|
|
15
|
+
SCREENSHOTS_DIR = 'docs/screenshots'
|
|
16
|
+
|
|
17
|
+
# Fallbacks for specs that omit the directives below. Specs with no
|
|
18
|
+
# `@category` collect under DEFAULT_CATEGORY; specs with no `@order` sort
|
|
19
|
+
# after numbered ones. Both defaults push such docs to the end of the TOC.
|
|
20
|
+
DEFAULT_CATEGORY = 'Other'
|
|
21
|
+
DEFAULT_ORDER = 999_999
|
|
22
|
+
|
|
23
|
+
# Metadata directives a spec places in its doc comments to control where it
|
|
24
|
+
# appears in the table of contents. They are parsed out of the comment stream
|
|
25
|
+
# and never rendered into the generated doc body, e.g.
|
|
26
|
+
#
|
|
27
|
+
# # @category Audits & Reports
|
|
28
|
+
# # @order 510
|
|
29
|
+
#
|
|
30
|
+
# # = Feature Name
|
|
31
|
+
#
|
|
32
|
+
# `@order` is a single global ordering value across all guides. The README
|
|
33
|
+
# groups guides by `@category` and lists categories in ascending order of the
|
|
34
|
+
# smallest `@order` they contain, so the entire structure is driven by the
|
|
35
|
+
# specs themselves — there is no category list to keep in sync here.
|
|
36
|
+
CATEGORY_DIRECTIVE = /^\s*#\s*@category\s+(.+?)\s*$/
|
|
37
|
+
ORDER_DIRECTIVE = /^\s*#\s*@order\s+(\d+)\s*$/
|
|
38
|
+
DIRECTIVE = /^\s*#\s*@(?:category|order)\b/
|
|
39
|
+
|
|
40
|
+
def initialize(spec_files = nil)
|
|
41
|
+
@spec_files = spec_files || find_documented_specs
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def generate(run_specs: true)
|
|
45
|
+
if @spec_files.empty?
|
|
46
|
+
puts 'No system specs with documentation comments found.'
|
|
47
|
+
return
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
@spec_files.each do |spec_file|
|
|
51
|
+
puts "\nGenerating documentation for #{spec_file}..."
|
|
52
|
+
process_spec(spec_file, run_specs:)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
generate_readme
|
|
56
|
+
|
|
57
|
+
puts "\n✓ Documentation generation complete!"
|
|
58
|
+
puts " Generated #{@spec_files.size} documentation file(s) in #{DOCS_DIR}/"
|
|
59
|
+
puts " Generated README.md with table of contents"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Verify the committed platform-guide docs are in sync with their documented
|
|
63
|
+
# specs, without running the specs. Re-renders the doc body (headings, prose,
|
|
64
|
+
# screenshot references) from each spec and compares it against the committed
|
|
65
|
+
# markdown, ignoring the generation timestamp and the screenshot binaries
|
|
66
|
+
# themselves. Returns true when everything is up to date.
|
|
67
|
+
def check
|
|
68
|
+
stale = find_documented_specs.select { |spec_file| doc_stale?(spec_file) }
|
|
69
|
+
|
|
70
|
+
if stale.empty?
|
|
71
|
+
puts '✓ Platform-guide docs are up to date.'
|
|
72
|
+
return true
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
warn "✗ Platform-guide docs are out of date for #{stale.size} spec(s):"
|
|
76
|
+
stale.each { |spec_file| warn " - #{spec_file} -> #{generate_output_filename(spec_file)}" }
|
|
77
|
+
warn ''
|
|
78
|
+
warn 'Regenerate the affected guide(s) and commit the result:'
|
|
79
|
+
stale.each { |spec_file| warn " bin/run bin/generate-docs #{spec_file}" }
|
|
80
|
+
false
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Rebuild only the README table of contents from the committed docs and their
|
|
84
|
+
# spec `@category`/`@order` directives, without running any specs. Used after
|
|
85
|
+
# changing directives or category ordering.
|
|
86
|
+
def regenerate_readme
|
|
87
|
+
generate_readme
|
|
88
|
+
puts '✓ Regenerated README.md table of contents.'
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
private
|
|
92
|
+
|
|
93
|
+
def doc_stale?(spec_file)
|
|
94
|
+
output_file = generate_output_filename(spec_file)
|
|
95
|
+
return true unless File.exist?(output_file)
|
|
96
|
+
|
|
97
|
+
expected = "#{render_doc_body(extract_doc_comments(spec_file), extract_screenshot_calls(spec_file)).rstrip}\n"
|
|
98
|
+
committed = strip_timestamp_header(File.read(output_file, encoding: 'UTF-8'))
|
|
99
|
+
expected != committed
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def strip_timestamp_header(content)
|
|
103
|
+
content.sub(/\A<!--\nGenerated: [^\n]*\n-->\n\n/, '')
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def find_documented_specs
|
|
107
|
+
Dir.glob('spec/system/**/*_spec.rb').select do |file|
|
|
108
|
+
File.read(file, encoding: 'UTF-8').match?(/^#\s*=/)
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def process_spec(spec_file, run_specs: true)
|
|
113
|
+
# Extract documentation comments and screenshot calls
|
|
114
|
+
doc_comments = extract_doc_comments(spec_file)
|
|
115
|
+
screenshot_calls = extract_screenshot_calls(spec_file)
|
|
116
|
+
|
|
117
|
+
if run_specs
|
|
118
|
+
# Run the spec with screenshots enabled
|
|
119
|
+
puts " Running spec with SCREENSHOTS=true..."
|
|
120
|
+
run_spec_with_screenshots(spec_file)
|
|
121
|
+
else
|
|
122
|
+
puts " Re-rendering from doc comments (using existing screenshots)..."
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Generate markdown file
|
|
126
|
+
output_file = generate_output_filename(spec_file)
|
|
127
|
+
generate_markdown(output_file, doc_comments, screenshot_calls)
|
|
128
|
+
|
|
129
|
+
puts " ✓ Generated #{output_file}"
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def extract_doc_comments(spec_file)
|
|
133
|
+
content = File.read(spec_file, encoding: 'UTF-8')
|
|
134
|
+
lines = content.split("\n")
|
|
135
|
+
comments = []
|
|
136
|
+
current_block = { level: 0, title: '', content: [], line_number: 0 }
|
|
137
|
+
|
|
138
|
+
lines.each_with_index do |line, index|
|
|
139
|
+
if line =~ DIRECTIVE
|
|
140
|
+
# @category / @order are TOC metadata, not body content — skip them.
|
|
141
|
+
next
|
|
142
|
+
elsif line =~ /^(\s*)#\s*(=+)\s*(.+)/
|
|
143
|
+
# Save previous block if it exists
|
|
144
|
+
comments << current_block.dup unless current_block[:title].empty?
|
|
145
|
+
|
|
146
|
+
# Start new section
|
|
147
|
+
level = Regexp.last_match(2).length
|
|
148
|
+
title = Regexp.last_match(3).strip
|
|
149
|
+
current_block = { level:, title:, content: [], line_number: index + 1 }
|
|
150
|
+
elsif line =~ /^(\s*)#\s*(.*)/ && !current_block[:title].empty?
|
|
151
|
+
# Add to current block
|
|
152
|
+
content_line = Regexp.last_match(2).strip
|
|
153
|
+
current_block[:content] << content_line
|
|
154
|
+
elsif !current_block[:title].empty? && line !~ /^(\s*)#/
|
|
155
|
+
# End of comment block
|
|
156
|
+
comments << current_block.dup
|
|
157
|
+
current_block = { level: 0, title: '', content: [], line_number: 0 }
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Save last block if needed
|
|
162
|
+
comments << current_block unless current_block[:title].empty?
|
|
163
|
+
|
|
164
|
+
comments
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def extract_screenshot_calls(spec_file)
|
|
168
|
+
content = File.read(spec_file, encoding: 'UTF-8')
|
|
169
|
+
lines = content.split("\n")
|
|
170
|
+
screenshots = []
|
|
171
|
+
|
|
172
|
+
lines.each_with_index do |line, index|
|
|
173
|
+
if line =~ /doc_screenshot\(['"]([\w\-\/]+)['"]\)/
|
|
174
|
+
screenshots << { path: Regexp.last_match(1), line_number: index + 1 }
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
screenshots
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def run_spec_with_screenshots(spec_file)
|
|
182
|
+
# Ensure screenshots directory exists
|
|
183
|
+
FileUtils.mkdir_p(SCREENSHOTS_DIR)
|
|
184
|
+
|
|
185
|
+
# Run the spec
|
|
186
|
+
command = "SCREENSHOTS=true bundle exec rspec --format documentation #{spec_file}"
|
|
187
|
+
system(command, err: File::NULL)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def generate_output_filename(spec_file)
|
|
191
|
+
basename = File.basename(spec_file, '_spec.rb')
|
|
192
|
+
output_name = basename.upcase.gsub('_', '-') + '.md'
|
|
193
|
+
File.join(DOCS_DIR, output_name)
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def generate_markdown(output_file, doc_comments, screenshot_calls)
|
|
197
|
+
FileUtils.mkdir_p(DOCS_DIR)
|
|
198
|
+
File.write(output_file, "#{timestamp_header}#{render_doc_body(doc_comments, screenshot_calls).rstrip}\n")
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def timestamp_header
|
|
202
|
+
"<!--\nGenerated: #{Time.now.utc.strftime('%Y-%m-%d %H:%M:%S UTC')}\n-->\n\n"
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# Render the markdown body (everything after the timestamp header) from a
|
|
206
|
+
# spec's doc comments and screenshot calls. Shared by generation and --check
|
|
207
|
+
# so both produce byte-identical output for the same spec.
|
|
208
|
+
def render_doc_body(doc_comments, screenshot_calls)
|
|
209
|
+
body = +''
|
|
210
|
+
|
|
211
|
+
doc_comments.each_with_index do |comment, index|
|
|
212
|
+
heading_tag = "h#{comment[:level]}"
|
|
213
|
+
heading_id = slugify(comment[:title])
|
|
214
|
+
body << "<#{heading_tag} id=\"#{heading_id}\">#{comment[:title]}</#{heading_tag}>\n\n"
|
|
215
|
+
|
|
216
|
+
comment[:content].each { |line| body << "#{line}\n" }
|
|
217
|
+
body << "\n"
|
|
218
|
+
|
|
219
|
+
next_comment_line = doc_comments[index + 1]&.dig(:line_number) || Float::INFINITY
|
|
220
|
+
screenshots = find_related_screenshots(comment[:line_number], next_comment_line, screenshot_calls)
|
|
221
|
+
screenshots.each do |screenshot|
|
|
222
|
+
rendered = render_screenshot(screenshot[:path])
|
|
223
|
+
body << rendered if rendered
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
body
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def find_related_screenshots(current_line, next_line, screenshot_calls)
|
|
231
|
+
# Find screenshots that appear after this comment but before the next one
|
|
232
|
+
screenshot_calls.select do |screenshot|
|
|
233
|
+
screenshot[:line_number] > current_line && screenshot[:line_number] < next_line
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def render_screenshot(screenshot_path)
|
|
238
|
+
# Only embed screenshots whose generated file exists on disk
|
|
239
|
+
return unless find_screenshot_file(screenshot_path)
|
|
240
|
+
|
|
241
|
+
# Use full path from docs/platform-guide to docs/screenshots
|
|
242
|
+
"\n\n"
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def find_screenshot_file(screenshot_path)
|
|
246
|
+
# Screenshots are saved in docs/screenshots during test run
|
|
247
|
+
screenshot_file = File.join(SCREENSHOTS_DIR, "#{screenshot_path}.png")
|
|
248
|
+
|
|
249
|
+
if File.exist?(screenshot_file)
|
|
250
|
+
screenshot_file
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def slugify(text)
|
|
255
|
+
text.downcase
|
|
256
|
+
.gsub(/[^a-z0-9\s-]/, '')
|
|
257
|
+
.gsub(/\s+/, '-')
|
|
258
|
+
.gsub(/-+/, '-')
|
|
259
|
+
.gsub(/^-|-$/, '')
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
# Writes the README table of contents grouped by `@category` and ordered by
|
|
263
|
+
# `@order`. Called at the end of a full/partial generation run and standalone
|
|
264
|
+
# via `regenerate_readme` (the `--readme-only` mode).
|
|
265
|
+
def generate_readme
|
|
266
|
+
readme_path = File.join(DOCS_DIR, 'README.md')
|
|
267
|
+
|
|
268
|
+
File.open(readme_path, 'w') do |f|
|
|
269
|
+
# Add generation timestamp
|
|
270
|
+
f.puts "<!--"
|
|
271
|
+
f.puts "Generated: #{Time.now.utc.strftime('%Y-%m-%d %H:%M:%S UTC')}"
|
|
272
|
+
f.puts "-->"
|
|
273
|
+
f.puts
|
|
274
|
+
|
|
275
|
+
f.puts '<h1 id="platform-guide">Platform Guide</h1>'
|
|
276
|
+
f.puts
|
|
277
|
+
f.puts 'This guide is the primary, automatically generated reference for operating the'
|
|
278
|
+
f.puts 'platform. It is extracted from the admin system specs, so every page reflects the'
|
|
279
|
+
f.puts 'behaviour that is actually tested in the codebase.'
|
|
280
|
+
f.puts
|
|
281
|
+
f.puts 'Guides are grouped by area below and ordered to follow a typical admin workflow —'
|
|
282
|
+
f.puts 'from onboarding a client through to running audits and imports.'
|
|
283
|
+
|
|
284
|
+
# Build the TOC from every documented spec (not just the ones regenerated
|
|
285
|
+
# in this run) so running for a subset of specs doesn't drop other
|
|
286
|
+
# entries. Group by `@category` and order by `@order`.
|
|
287
|
+
groups = toc_groups
|
|
288
|
+
|
|
289
|
+
f.puts category_workflow_diagram(groups.map(&:first))
|
|
290
|
+
f.puts
|
|
291
|
+
f.puts '<h2 id="table-of-contents">Table of Contents</h2>'
|
|
292
|
+
|
|
293
|
+
groups.each do |category, entries|
|
|
294
|
+
f.puts
|
|
295
|
+
f.puts %(<h3 id="#{slugify(category)}">#{category}</h3>)
|
|
296
|
+
f.puts
|
|
297
|
+
entries.each do |entry|
|
|
298
|
+
f.puts "- [#{entry[:title]}](#{entry[:file]})"
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
# A Mermaid flowchart of the guide categories in workflow order, rendered by
|
|
305
|
+
# the in-app docs viewer. Derived from the same ordered categories as the TOC,
|
|
306
|
+
# so it never drifts from the actual guides.
|
|
307
|
+
def category_workflow_diagram(categories)
|
|
308
|
+
return '' if categories.empty?
|
|
309
|
+
|
|
310
|
+
nodes = categories.each_with_index.map { |category, index| %(c#{index}["#{category}"]) }
|
|
311
|
+
<<~DIAGRAM.chomp
|
|
312
|
+
|
|
313
|
+
```mermaid
|
|
314
|
+
flowchart LR
|
|
315
|
+
#{nodes.join(' --> ')}
|
|
316
|
+
```
|
|
317
|
+
DIAGRAM
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
# Returns the table of contents as an ordered list of [category, entries]
|
|
321
|
+
# pairs. Category order is derived entirely from the specs: categories are
|
|
322
|
+
# listed in ascending order of the smallest `@order` they contain (ties broken
|
|
323
|
+
# alphabetically), and entries within a category are ordered by `@order` then
|
|
324
|
+
# title. Only specs whose generated doc actually exists on disk are linked.
|
|
325
|
+
def toc_groups
|
|
326
|
+
entries = find_documented_specs.filter_map do |spec_file|
|
|
327
|
+
file = File.basename(generate_output_filename(spec_file))
|
|
328
|
+
next unless File.exist?(File.join(DOCS_DIR, file))
|
|
329
|
+
|
|
330
|
+
directives = extract_directives(spec_file)
|
|
331
|
+
{
|
|
332
|
+
file:,
|
|
333
|
+
title: spec_doc_title(spec_file),
|
|
334
|
+
category: directives[:category] || DEFAULT_CATEGORY,
|
|
335
|
+
order: directives[:order] || DEFAULT_ORDER
|
|
336
|
+
}
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
entries
|
|
340
|
+
.group_by { |entry| entry[:category] }
|
|
341
|
+
.sort_by { |category, group| [group.map { |entry| entry[:order] }.min, category] }
|
|
342
|
+
.map { |category, group| [category, group.sort_by { |entry| [entry[:order], entry[:title]] }] }
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
# Read the `@category` / `@order` directives from a spec's doc comments. These
|
|
346
|
+
# control TOC placement only and are stripped from the rendered doc body.
|
|
347
|
+
def extract_directives(spec_file)
|
|
348
|
+
category = nil
|
|
349
|
+
order = nil
|
|
350
|
+
|
|
351
|
+
File.foreach(spec_file, encoding: 'UTF-8') do |line|
|
|
352
|
+
category ||= Regexp.last_match(1) if line =~ CATEGORY_DIRECTIVE
|
|
353
|
+
order ||= Regexp.last_match(1).to_i if line =~ ORDER_DIRECTIVE
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
{ category:, order: }
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
# The doc title is the first top-level (`# =`, not `# ==`) heading comment.
|
|
360
|
+
def spec_doc_title(spec_file)
|
|
361
|
+
File.foreach(spec_file, encoding: 'UTF-8') do |line|
|
|
362
|
+
return Regexp.last_match(1).strip if line =~ /^\s*#\s*=(?!=)\s*(.+?)\s*$/
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
File.basename(spec_file, '_spec.rb').split('_').map(&:capitalize).join(' ')
|
|
366
|
+
end
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
# Parse command line arguments
|
|
370
|
+
args = ARGV.dup
|
|
371
|
+
check_mode = args.delete('--check')
|
|
372
|
+
readme_only = args.delete('--readme-only')
|
|
373
|
+
render_only = args.delete('--render-only')
|
|
374
|
+
spec_files = args.empty? ? nil : args
|
|
375
|
+
|
|
376
|
+
generator = DocGenerator.new(spec_files)
|
|
377
|
+
|
|
378
|
+
if check_mode
|
|
379
|
+
# Used by the `docs:check` rake task — verify docs without running specs.
|
|
380
|
+
exit(generator.check ? 0 : 1)
|
|
381
|
+
elsif readme_only
|
|
382
|
+
# Rebuild the table of contents from committed docs + spec directives only,
|
|
383
|
+
# without running any specs (fast; use after changing @category/@order).
|
|
384
|
+
generator.regenerate_readme
|
|
385
|
+
elsif render_only
|
|
386
|
+
# Re-render doc bodies from doc comments using the screenshots already on
|
|
387
|
+
# disk, without running any specs. Use after prose-only doc-comment edits
|
|
388
|
+
# (Mermaid diagrams, tables, blockquotes) that don't change screenshots.
|
|
389
|
+
generator.generate(run_specs: false)
|
|
390
|
+
else
|
|
391
|
+
generator.generate
|
|
392
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Helper method for conditional screenshot capture
|
|
4
|
+
# Usage: doc_screenshot('01_page_name')
|
|
5
|
+
# Enable with: SCREENSHOTS=true rspec spec/...
|
|
6
|
+
module DocScreenshotHelper
|
|
7
|
+
def doc_screenshot(filename)
|
|
8
|
+
return unless ENV["SCREENSHOTS"] == "true"
|
|
9
|
+
|
|
10
|
+
sleep 0.5 # Allow time for animations to complete
|
|
11
|
+
save_screenshot(Rails.root.join("docs/screenshots/#{filename}.png")) # rubocop:disable Lint/Debugger
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
RSpec.configure do |config|
|
|
16
|
+
config.include DocScreenshotHelper, type: :system
|
|
17
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails_helper"
|
|
4
|
+
|
|
5
|
+
#
|
|
6
|
+
# @category Getting Started
|
|
7
|
+
# @order 100
|
|
8
|
+
|
|
9
|
+
# = Example Documentation Spec
|
|
10
|
+
#
|
|
11
|
+
# This spec demonstrates the self-documenting system spec pattern.
|
|
12
|
+
# Doc comments starting with `# =` become headings in the generated markdown.
|
|
13
|
+
# Use `# ==` for sub-headings, `# ===` for sub-sub-headings, etc.
|
|
14
|
+
#
|
|
15
|
+
# The `@category` and `@order` directives control where this guide appears
|
|
16
|
+
# in the table of contents generated by `bin/generate-docs`.
|
|
17
|
+
#
|
|
18
|
+
# == Overview
|
|
19
|
+
#
|
|
20
|
+
# 1. Write doc comments above your system spec using `# = Heading` syntax
|
|
21
|
+
# 2. Call `doc_screenshot('category/screenshot_name')` to capture screenshots
|
|
22
|
+
# 3. Run `bin/generate-docs` to generate markdown documentation
|
|
23
|
+
# 4. Run `rake docs:check` to verify docs are in sync with specs
|
|
24
|
+
#
|
|
25
|
+
describe "Example Documentation" do
|
|
26
|
+
before do
|
|
27
|
+
visit "/docs"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
it "displays the homepage" do
|
|
31
|
+
expect(page).to have_css("body")
|
|
32
|
+
|
|
33
|
+
# === Homepage Screenshot
|
|
34
|
+
#
|
|
35
|
+
# This screenshot captures the homepage as seen by users.
|
|
36
|
+
doc_screenshot("getting-started/homepage")
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "develoz"
|
|
4
|
+
|
|
5
|
+
module Develoz
|
|
6
|
+
module Generators
|
|
7
|
+
class DockerGenerator < Develoz::Generators::Base
|
|
8
|
+
def self.source_root
|
|
9
|
+
File.expand_path("templates", __dir__)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def create_docker_compose
|
|
13
|
+
template "docker-compose.yml.tt", "docker-compose.yml"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def create_dockerfile_dev
|
|
17
|
+
template "dockerfile_dev.tt", "Dockerfile.dev"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def create_bin_dev
|
|
21
|
+
template "bin_dev.tt", "bin/dev"
|
|
22
|
+
chmod "bin/dev", 0o755
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def create_bin_setup
|
|
26
|
+
template "bin_setup.tt", "bin/setup"
|
|
27
|
+
chmod "bin/setup", 0o755
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def create_bin_docker_entrypoint
|
|
31
|
+
template "bin_docker_entrypoint.tt", "bin/docker-entrypoint"
|
|
32
|
+
chmod "bin/docker-entrypoint", 0o755
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def create_bin_run
|
|
36
|
+
template "bin_run.tt", "bin/run"
|
|
37
|
+
chmod "bin/run", 0o755
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def wire_env
|
|
41
|
+
append_env "POSTGRES_USER", "postgres"
|
|
42
|
+
append_env "POSTGRES_PASSWORD", "postgres"
|
|
43
|
+
append_env "POSTGRES_DB", "#{app_name}_development"
|
|
44
|
+
append_env "DATABASE_URL", "postgres://postgres:postgres@postgres:5432/#{app_name}_development"
|
|
45
|
+
append_env "SELENIUM_URL", "http://selenium:4444/wd/hub"
|
|
46
|
+
append_env "MAILCATCHER_URL", "http://mailcatcher:1080"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def ensure_env_gitignored
|
|
50
|
+
ensure_gitignore(".env")
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
# Docker-aware command runner
|
|
4
|
+
# Runs commands locally or in Docker containers based on availability
|
|
5
|
+
|
|
6
|
+
set -e
|
|
7
|
+
|
|
8
|
+
docker_available() {
|
|
9
|
+
command -v docker >/dev/null 2>&1
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
docker_accessible() {
|
|
13
|
+
local output
|
|
14
|
+
|
|
15
|
+
if ! output=$(docker compose ps --services 2>/dev/null 2>&1); then
|
|
16
|
+
docker_error="$output"
|
|
17
|
+
return 1
|
|
18
|
+
fi
|
|
19
|
+
|
|
20
|
+
true
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
container_running() {
|
|
24
|
+
local service="$1"
|
|
25
|
+
local output
|
|
26
|
+
|
|
27
|
+
if ! output=$(docker compose ps "$service" --services 2>/dev/null 2>&1); then
|
|
28
|
+
docker_error="$output"
|
|
29
|
+
return 1
|
|
30
|
+
fi
|
|
31
|
+
|
|
32
|
+
echo "$output" | grep -q "$service"
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
container_built() {
|
|
36
|
+
local service="$1"
|
|
37
|
+
local output
|
|
38
|
+
|
|
39
|
+
if ! output=$(docker compose ps -a "$service" 2>/dev/null 2>&1); then
|
|
40
|
+
docker_error="$output"
|
|
41
|
+
return 1
|
|
42
|
+
fi
|
|
43
|
+
|
|
44
|
+
echo "$output" | grep -q "$service"
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
determine_service() {
|
|
48
|
+
local container_override="$1"
|
|
49
|
+
|
|
50
|
+
if [[ -n "$container_override" ]]; then
|
|
51
|
+
echo "$container_override"
|
|
52
|
+
else
|
|
53
|
+
echo "app"
|
|
54
|
+
fi
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
run_locally() {
|
|
58
|
+
local service="$1"
|
|
59
|
+
shift
|
|
60
|
+
echo "Running locally: $*"
|
|
61
|
+
|
|
62
|
+
# Source asdf environment
|
|
63
|
+
if [[ -d "$HOME/.asdf" ]]; then
|
|
64
|
+
export ASDF_DATA_DIR="$HOME/.asdf"
|
|
65
|
+
export PATH="$HOME/.asdf/bin:$HOME/.asdf/shims:$PATH"
|
|
66
|
+
fi
|
|
67
|
+
|
|
68
|
+
# Source rbenv if available
|
|
69
|
+
if [[ -d "$HOME/.rbenv" ]]; then
|
|
70
|
+
export PATH="$HOME/.rbenv/bin:$PATH"
|
|
71
|
+
eval "$(rbenv init -)"
|
|
72
|
+
fi
|
|
73
|
+
|
|
74
|
+
# Source RVM if available
|
|
75
|
+
if [[ -d "$HOME/.rvm" ]]; then
|
|
76
|
+
source "$HOME/.rvm/scripts/rvm"
|
|
77
|
+
fi
|
|
78
|
+
|
|
79
|
+
exec "$@"
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
run_with_docker() {
|
|
83
|
+
local service="$1"
|
|
84
|
+
shift
|
|
85
|
+
local -a mode=()
|
|
86
|
+
while [[ "$1" != "--" ]]; do
|
|
87
|
+
mode+=("$1")
|
|
88
|
+
shift
|
|
89
|
+
done
|
|
90
|
+
shift # consume --
|
|
91
|
+
|
|
92
|
+
echo "Running: docker compose ${mode[*]} $service $*"
|
|
93
|
+
exec docker compose "${mode[@]}" "$service" "$@"
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if [[ $# -eq 0 ]]; then
|
|
97
|
+
echo "Usage: bin/run [--container <name>] <command>"
|
|
98
|
+
echo "Options:"
|
|
99
|
+
echo " --container <name> Run in a specific container (default: app)"
|
|
100
|
+
echo ""
|
|
101
|
+
echo "Examples:"
|
|
102
|
+
echo " bin/run rspec"
|
|
103
|
+
echo " bin/run rubocop -a"
|
|
104
|
+
echo " bin/run rake db:migrate"
|
|
105
|
+
echo " bin/run --container implementation bin/process"
|
|
106
|
+
exit 1
|
|
107
|
+
fi
|
|
108
|
+
|
|
109
|
+
# Parse --container flag
|
|
110
|
+
container_override=""
|
|
111
|
+
args=()
|
|
112
|
+
while [[ $# -gt 0 ]]; do
|
|
113
|
+
case "$1" in
|
|
114
|
+
--container)
|
|
115
|
+
if [[ -z "$2" || "$2" == --* ]]; then
|
|
116
|
+
echo "Error: --container requires a container name"
|
|
117
|
+
exit 1
|
|
118
|
+
fi
|
|
119
|
+
container_override="$2"
|
|
120
|
+
shift 2
|
|
121
|
+
;;
|
|
122
|
+
*)
|
|
123
|
+
args+=("$1")
|
|
124
|
+
shift
|
|
125
|
+
;;
|
|
126
|
+
esac
|
|
127
|
+
done
|
|
128
|
+
|
|
129
|
+
if [[ ${#args[@]} -eq 0 ]]; then
|
|
130
|
+
echo "Error: No command specified"
|
|
131
|
+
exit 1
|
|
132
|
+
fi
|
|
133
|
+
|
|
134
|
+
service=$(determine_service "$container_override")
|
|
135
|
+
|
|
136
|
+
if ! docker_available; then
|
|
137
|
+
echo "Docker not available, running locally"
|
|
138
|
+
run_locally "$service" "${args[@]}"
|
|
139
|
+
elif ! docker_accessible; then
|
|
140
|
+
echo "Docker is installed but not accessible: ${docker_error:-unknown error}"
|
|
141
|
+
echo "Ensure Docker is running and you have permission to access /var/run/docker.sock."
|
|
142
|
+
run_locally "$service" "${args[@]}"
|
|
143
|
+
elif container_running "$service"; then
|
|
144
|
+
echo "Container $service is running"
|
|
145
|
+
run_with_docker "$service" exec -- "${args[@]}"
|
|
146
|
+
elif container_built "$service"; then
|
|
147
|
+
echo "Container $service is built but stopped"
|
|
148
|
+
run_with_docker "$service" run -e SKIP_MIGRATIONS=true --rm -- "${args[@]}"
|
|
149
|
+
else
|
|
150
|
+
echo "Container $service not available, running locally"
|
|
151
|
+
run_locally "$service" "${args[@]}"
|
|
152
|
+
fi
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
echo "==> Setting up development environment..."
|
|
5
|
+
|
|
6
|
+
# Install gems
|
|
7
|
+
bundle install
|
|
8
|
+
|
|
9
|
+
# Create and migrate the database
|
|
10
|
+
if [ -f config/database.yml ]; then
|
|
11
|
+
bin/rails db:create db:migrate
|
|
12
|
+
fi
|
|
13
|
+
|
|
14
|
+
echo "==> Setup complete!"
|
|
15
|
+
echo "Run bin/dev to start the development server."
|