gemstack-cli 0.1.0 → 0.2.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 +4 -0
- data/lib/gemstack/cli/app_generator.rb +26 -3
- data/lib/gemstack/cli/commands/db.rb +4 -3
- data/lib/gemstack/cli/commands/jobs.rb +4 -4
- data/lib/gemstack/cli/deploy_generator.rb +39 -0
- data/lib/gemstack/cli/doctor.rb +38 -8
- data/lib/gemstack/cli.rb +3 -1
- data/templates/app/Gemfile.tt +3 -0
- data/templates/app/config/app.rb.tt +1 -1
- data/templates/app/config/database.yml.tt +61 -0
- data/templates/app/dot_env.example.tt +19 -0
- data/templates/app/dot_gitignore +2 -0
- data/templates/auth/db/migrations/%timestamp%_create_auth_tables.rb +8 -8
- data/templates/deploy/Dockerfile.tt +4 -3
- data/templates/deploy/compose.yaml.tt +51 -2
- metadata +7 -6
- data/templates/app/dot_env.example +0 -9
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 302c6d867a26dcdbf305cc2c4d54479bc3b5094b30dc7c79873882b5d6062598
|
|
4
|
+
data.tar.gz: f09ed00f2ac675747ab91c808a682b75c71be04193defc59755e7798c0dbfc49
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 94dddfd035b6782455385e61a8306bcb46526e5979c43f43b4604cb133593713b0166528df8345ab6850f288c7f6118e75381a91c5729c9dcba2977e12819d81
|
|
7
|
+
data.tar.gz: cae54dd8b24e7ce6ba106570d858476992c6b11634e717e9ef2b79b1ac1d853ea3292bd32b96994a65e98cc22320bbc860d55d781843c289320cccc6902deaaa
|
data/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,12 @@ module GemStack
|
|
|
6
6
|
# framework infrastructure and deliberately no business resources.
|
|
7
7
|
class AppGenerator < Generator
|
|
8
8
|
NAME = /\A[a-z][a-z0-9_-]*\z/
|
|
9
|
+
# `--database` values (Rails' names) and aliases.
|
|
10
|
+
DATABASES = { "sqlite3" => "sqlite3", "sqlite" => "sqlite3", "postgresql" => "postgresql",
|
|
11
|
+
"postgres" => "postgresql", "pg" => "postgresql", "mysql2" => "mysql2", "mysql" => "mysql2",
|
|
12
|
+
"trilogy" => "trilogy" }.freeze
|
|
13
|
+
DRIVERS = { "sqlite3" => ["sqlite3", "~> 2.0"], "postgresql" => ["pg", "~> 1.5"],
|
|
14
|
+
"mysql2" => ["mysql2", "~> 0.5"], "trilogy" => ["trilogy", "~> 2.9"] }.freeze
|
|
9
15
|
|
|
10
16
|
# The gems/ directory of the GemStack checkout this CLI runs from, if any.
|
|
11
17
|
CHECKOUT = File.expand_path("../../../..", __dir__)
|
|
@@ -29,10 +35,27 @@ module GemStack
|
|
|
29
35
|
def frontend? = !@options[:skip_frontend]
|
|
30
36
|
def database? = !@options[:skip_database]
|
|
31
37
|
|
|
38
|
+
# sqlite3 (default), postgresql, mysql2 or trilogy.
|
|
39
|
+
def database_adapter
|
|
40
|
+
@database_adapter ||= DATABASES.fetch(@options[:database].to_s.downcase.then do |d|
|
|
41
|
+
d.empty? ? "sqlite3" : d
|
|
42
|
+
end) do
|
|
43
|
+
raise Thor::Error,
|
|
44
|
+
"Unknown --database #{@options[:database].inspect}. Use: sqlite3, postgresql, mysql2, trilogy"
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def driver_gem = DRIVERS.fetch(database_adapter).first
|
|
49
|
+
def driver_version = DRIVERS.fetch(database_adapter).last
|
|
50
|
+
def database_name = name.tr("-", "_")
|
|
51
|
+
|
|
32
52
|
def run
|
|
33
53
|
validate!
|
|
54
|
+
database_adapter if database? # fail early on an unknown --database
|
|
34
55
|
@output.puts("Creating GemStack application #{name} in #{destination}")
|
|
35
|
-
render_directory("app", destination, skip:
|
|
56
|
+
render_directory("app", destination, skip: lambda { |rel|
|
|
57
|
+
!database? && (rel.start_with?("db/") || rel == "config/database.yml.tt")
|
|
58
|
+
})
|
|
36
59
|
render_directory("frontend", File.join(destination, "frontend")) if frontend?
|
|
37
60
|
install unless @options[:skip_install]
|
|
38
61
|
git_init unless @options[:skip_git]
|
|
@@ -65,14 +88,14 @@ module GemStack
|
|
|
65
88
|
"--no-audit")
|
|
66
89
|
end
|
|
67
90
|
|
|
68
|
-
# Best effort: a missing or password-protected
|
|
91
|
+
# Best effort: a missing or password-protected database server shouldn't fail `new`.
|
|
69
92
|
def create_database
|
|
70
93
|
@output.puts(" #{"run".rjust(9)} gemstack db:create")
|
|
71
94
|
ok = unbundled { system("bin/gemstack", "db:create", chdir: destination, out: File::NULL, err: File::NULL) }
|
|
72
95
|
return if ok
|
|
73
96
|
|
|
74
97
|
@output.puts(" #{"warning".rjust(9)} couldn't create the database — " \
|
|
75
|
-
"set DATABASE_URL in .env, then run `gemstack db:create`")
|
|
98
|
+
"check config/database.yml (or set DATABASE_URL in .env), then run `gemstack db:create`")
|
|
76
99
|
end
|
|
77
100
|
|
|
78
101
|
def git_init
|
|
@@ -8,7 +8,7 @@ module GemStack
|
|
|
8
8
|
|
|
9
9
|
class_option :environment, aliases: "-e", type: :string, desc: "Environment (default: GEMSTACK_ENV or development)"
|
|
10
10
|
|
|
11
|
-
desc "db:create", "Create the database (config.
|
|
11
|
+
desc "db:create", "Create the database (config/database.yml or DATABASE_URL)"
|
|
12
12
|
def db_create
|
|
13
13
|
with_database do
|
|
14
14
|
result = DB::Tasks.create
|
|
@@ -112,8 +112,9 @@ module GemStack
|
|
|
112
112
|
GemStack.boot! if boot
|
|
113
113
|
yield
|
|
114
114
|
rescue Sequel::DatabaseConnectionError => e
|
|
115
|
-
|
|
116
|
-
|
|
115
|
+
settings = DB.settings
|
|
116
|
+
abort("Can't connect to #{DB::Configuration.describe(settings)} (from #{settings[:source]}): " \
|
|
117
|
+
"#{e.message.lines.first.strip}\nCheck config/database.yml or DATABASE_URL — see docs/database.md.")
|
|
117
118
|
rescue GemStack::Error => e
|
|
118
119
|
abort(e.message)
|
|
119
120
|
end
|
|
@@ -25,7 +25,7 @@ module GemStack
|
|
|
25
25
|
desc "jobs:status", "Show queued, scheduled, running and failed jobs per queue"
|
|
26
26
|
def jobs_status
|
|
27
27
|
with_jobs(quiet: true) do
|
|
28
|
-
stats = Jobs::Adapters::
|
|
28
|
+
stats = Jobs::Adapters::Database.new.stats
|
|
29
29
|
return say("No jobs.") if stats.empty?
|
|
30
30
|
|
|
31
31
|
rows = stats.sort.map { |queue, s| [queue, s[:ready], s[:scheduled], s[:running], s[:failed]].map(&:to_s) }
|
|
@@ -37,7 +37,7 @@ module GemStack
|
|
|
37
37
|
method_option :limit, type: :numeric, default: 20
|
|
38
38
|
def jobs_failed
|
|
39
39
|
with_jobs(quiet: true) do
|
|
40
|
-
failed = Jobs::Adapters::
|
|
40
|
+
failed = Jobs::Adapters::Database.new.failed(limit: options[:limit])
|
|
41
41
|
return say("No failed jobs.") if failed.empty?
|
|
42
42
|
|
|
43
43
|
failed.each do |job|
|
|
@@ -51,7 +51,7 @@ module GemStack
|
|
|
51
51
|
desc "jobs:retry [IDS...]", "Put failed jobs back on the queue (all failed jobs without IDS)"
|
|
52
52
|
def jobs_retry(*ids)
|
|
53
53
|
with_jobs(quiet: true) do
|
|
54
|
-
count = Jobs::Adapters::
|
|
54
|
+
count = Jobs::Adapters::Database.new.retry_failed(ids.empty? ? nil : ids.map(&:to_i))
|
|
55
55
|
say("Re-queued #{count} job(s).")
|
|
56
56
|
end
|
|
57
57
|
end
|
|
@@ -59,7 +59,7 @@ module GemStack
|
|
|
59
59
|
desc "jobs:discard [IDS...]", "Delete failed jobs (all failed jobs without IDS)"
|
|
60
60
|
def jobs_discard(*ids)
|
|
61
61
|
with_jobs(quiet: true) do
|
|
62
|
-
count = Jobs::Adapters::
|
|
62
|
+
count = Jobs::Adapters::Database.new.discard_failed(ids.empty? ? nil : ids.map(&:to_i))
|
|
63
63
|
say("Deleted #{count} failed job(s).")
|
|
64
64
|
end
|
|
65
65
|
end
|
|
@@ -23,6 +23,45 @@ module GemStack
|
|
|
23
23
|
end
|
|
24
24
|
|
|
25
25
|
def mail? = gemfile.match?(/^\s*gem "gemstack-(mail|auth)"/)
|
|
26
|
+
def database? = gemfile.match?(/^\s*gem "gemstack-db"/)
|
|
27
|
+
def realtime? = gemfile.match?(/^\s*gem "gemstack-realtime"/)
|
|
28
|
+
|
|
29
|
+
# From config/database.yml (production, else the first adapter listed);
|
|
30
|
+
# apps without one use PostgreSQL, GemStack's original default.
|
|
31
|
+
def database_adapter
|
|
32
|
+
path = File.join(@root, "config/database.yml")
|
|
33
|
+
return "postgresql" unless File.file?(path)
|
|
34
|
+
|
|
35
|
+
text = File.read(path)
|
|
36
|
+
production = text[/^production:.*?(?=^\S|\z)/m].to_s
|
|
37
|
+
(production[/^\s+adapter:\s*(\w+)/, 1] || text[/^\s+adapter:\s*(\w+)/, 1] || "postgresql")
|
|
38
|
+
.then { |name| { "postgres" => "postgresql", "sqlite" => "sqlite3", "mysql" => "mysql2" }.fetch(name, name) }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def sqlite? = database_adapter == "sqlite3"
|
|
42
|
+
def mysql? = %w[mysql2 trilogy].include?(database_adapter)
|
|
43
|
+
def postgresql? = database_adapter == "postgresql"
|
|
44
|
+
# A Redis for realtime fan-out when the database can't do it (PostgreSQL uses NOTIFY).
|
|
45
|
+
def redis? = realtime? && !postgresql?
|
|
46
|
+
|
|
47
|
+
def build_packages
|
|
48
|
+
(%w[build-essential libyaml-dev
|
|
49
|
+
git] + { "postgresql" => ["libpq-dev"], "mysql2" => ["default-libmysqlclient-dev"] }.fetch(database_adapter,
|
|
50
|
+
[])).join(" ")
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def runtime_packages
|
|
54
|
+
(%w[libyaml-0-2
|
|
55
|
+
curl] + { "postgresql" => ["libpq5"], "mysql2" => ["libmariadb3"] }.fetch(database_adapter, [])).join(" ")
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def database_url
|
|
59
|
+
case database_adapter
|
|
60
|
+
when "sqlite3" then "sqlite3:/data/production.sqlite3"
|
|
61
|
+
when "postgresql" then "postgres://app:${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD}@db:5432/app"
|
|
62
|
+
else "#{database_adapter}://app:${MYSQL_PASSWORD:?set MYSQL_PASSWORD}@db:3306/app"
|
|
63
|
+
end
|
|
64
|
+
end
|
|
26
65
|
|
|
27
66
|
def ruby_version
|
|
28
67
|
pinned = File.file?(File.join(@root, ".ruby-version")) && File.read(File.join(@root, ".ruby-version")).strip
|
data/lib/gemstack/cli/doctor.rb
CHANGED
|
@@ -35,6 +35,7 @@ module GemStack
|
|
|
35
35
|
database_ok = check_database
|
|
36
36
|
check_migrations_and_jobs if database_ok
|
|
37
37
|
check_cache
|
|
38
|
+
check_realtime_broker
|
|
38
39
|
check_contract if database_ok
|
|
39
40
|
end
|
|
40
41
|
check_production_settings if @production
|
|
@@ -130,20 +131,36 @@ module GemStack
|
|
|
130
131
|
def check_database
|
|
131
132
|
return true unless defined?(GemStack::DB)
|
|
132
133
|
|
|
133
|
-
|
|
134
|
+
settings = GemStack::DB.settings
|
|
135
|
+
where = "#{GemStack::DB::Configuration.describe(settings)}, from #{settings[:source]}"
|
|
136
|
+
if settings[:adapter] == "sqlite" && !GemStack::DB::Tasks.exists?(settings)
|
|
137
|
+
problem("SQLite database file missing (#{where})", "run: gemstack db:create")
|
|
138
|
+
return false
|
|
139
|
+
end
|
|
134
140
|
GemStack::DB.connection.test_connection
|
|
135
|
-
pass("
|
|
141
|
+
pass("database reachable (#{where})")
|
|
136
142
|
true
|
|
137
|
-
rescue StandardError => e
|
|
143
|
+
rescue StandardError, LoadError => e
|
|
138
144
|
message = e.message.lines.first.to_s.strip
|
|
139
145
|
message = message.split(/FATAL:\s*/, 2).last if message.include?("FATAL:")
|
|
140
|
-
hint = if message.
|
|
141
|
-
|
|
146
|
+
hint = if message.match?(/does not exist|Unknown database/) then "run: gemstack db:create"
|
|
147
|
+
elsif e.is_a?(GemStack::ConfigurationError) then "fix config/database.yml or DATABASE_URL"
|
|
148
|
+
else "is the database server running? Check config/database.yml or DATABASE_URL (#{where})"
|
|
142
149
|
end
|
|
143
|
-
problem("
|
|
150
|
+
problem("database: #{message}", hint)
|
|
144
151
|
false
|
|
145
152
|
end
|
|
146
153
|
|
|
154
|
+
# MySQL/SQLite apps: a jobs worker is a separate process, and the memory
|
|
155
|
+
# broker can't carry its broadcasts to the web server.
|
|
156
|
+
def check_realtime_broker
|
|
157
|
+
return unless defined?(GemStack::Realtime) && defined?(GemStack::Jobs) && GemStack::Jobs.database_queue?
|
|
158
|
+
return unless GemStack.config.realtime.broker.to_s == "memory" && jobs_used?
|
|
159
|
+
|
|
160
|
+
caution("realtime uses the in-process memory broker",
|
|
161
|
+
"broadcasts from jobs won't reach the web server — set config.realtime.broker = :redis (REDIS_URL)")
|
|
162
|
+
end
|
|
163
|
+
|
|
147
164
|
def check_migrations_and_jobs
|
|
148
165
|
pending = GemStack::DB::Migrator.new.pending
|
|
149
166
|
if pending.empty?
|
|
@@ -151,7 +168,7 @@ module GemStack
|
|
|
151
168
|
else
|
|
152
169
|
caution("#{pending.size} pending migration(s)", "run: gemstack db:migrate")
|
|
153
170
|
end
|
|
154
|
-
return unless defined?(GemStack::Jobs) && GemStack.
|
|
171
|
+
return unless defined?(GemStack::Jobs) && GemStack::Jobs.database_queue?
|
|
155
172
|
return unless jobs_used?
|
|
156
173
|
|
|
157
174
|
if GemStack::DB.connection.table_exists?(:gemstack_jobs)
|
|
@@ -194,7 +211,7 @@ module GemStack
|
|
|
194
211
|
elsif secret.length < 64 then problem("SECRET_KEY_BASE is too short", "use: openssl rand -hex 64")
|
|
195
212
|
else pass("SECRET_KEY_BASE is set")
|
|
196
213
|
end
|
|
197
|
-
|
|
214
|
+
check_production_database if defined?(GemStack::DB)
|
|
198
215
|
if defined?(GemStack::Mail) && GemStack.config.mail.delivery.to_s == "smtp"
|
|
199
216
|
env_check("SMTP_URL", "e.g. smtp://user:password@smtp.example.com:587")
|
|
200
217
|
end
|
|
@@ -205,6 +222,19 @@ module GemStack
|
|
|
205
222
|
env_check("REDIS_URL", "the Redis used by the cache")
|
|
206
223
|
end
|
|
207
224
|
|
|
225
|
+
def check_production_database
|
|
226
|
+
settings = GemStack::DB.settings
|
|
227
|
+
if settings[:source].start_with?("default")
|
|
228
|
+
problem("no production database configured",
|
|
229
|
+
"set DATABASE_URL, or a production section in config/database.yml")
|
|
230
|
+
elsif settings[:adapter] == "sqlite"
|
|
231
|
+
caution("SQLite in production (#{settings[:database]})",
|
|
232
|
+
"keep the file on a persistent volume and run a single server; back it up")
|
|
233
|
+
else
|
|
234
|
+
pass("production database: #{GemStack::DB::Configuration.describe(settings)} (#{settings[:source]})")
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
|
|
208
238
|
def check_storage_settings
|
|
209
239
|
return unless GemStack.config.storage.service.to_s == "s3"
|
|
210
240
|
|
data/lib/gemstack/cli.rb
CHANGED
|
@@ -53,7 +53,9 @@ module GemStack
|
|
|
53
53
|
method_option :skip_install, type: :boolean, default: false, desc: "Don't run bundle install / npm install"
|
|
54
54
|
method_option :skip_git, type: :boolean, default: false, desc: "Don't initialise a git repository"
|
|
55
55
|
method_option :skip_frontend, type: :boolean, default: false, desc: "API only: no Next.js frontend"
|
|
56
|
-
method_option :
|
|
56
|
+
method_option :database, aliases: "-d", type: :string, default: "sqlite3",
|
|
57
|
+
desc: "sqlite3 (default), postgresql, mysql2 or trilogy"
|
|
58
|
+
method_option :skip_database, type: :boolean, default: false, desc: "No database / gemstack-db"
|
|
57
59
|
method_option :gemstack_path, type: :string, desc: "Use GemStack from a local checkout (its gems/ directory)"
|
|
58
60
|
def new(name)
|
|
59
61
|
AppGenerator.new(name, options.transform_keys(&:to_sym)).run
|
data/templates/app/Gemfile.tt
CHANGED
|
@@ -26,6 +26,9 @@ gem "gemstack-db", "~> <%= version %>"
|
|
|
26
26
|
gem "gemstack-jobs", "~> <%= version %>"
|
|
27
27
|
<% end -%>
|
|
28
28
|
gem "puma", ">= 6.4"
|
|
29
|
+
<% if database? -%>
|
|
30
|
+
gem "<%= driver_gem %>", "<%= driver_version %>" # the <%= database_adapter %> driver (config/database.yml)
|
|
31
|
+
<% end -%>
|
|
29
32
|
|
|
30
33
|
# Any gem works here as usual, e.g.:
|
|
31
34
|
# gem "brotli" # Brotli compression (gzip is used without it)
|
|
@@ -23,7 +23,7 @@ GemStack.configure do |config|
|
|
|
23
23
|
# config.filter_parameters += %w[iban] # masked in logs
|
|
24
24
|
<% if database? -%>
|
|
25
25
|
#
|
|
26
|
-
# config.
|
|
26
|
+
# Connection: config/database.yml (DATABASE_URL / TEST_DATABASE_URL override it).
|
|
27
27
|
# config.db.pool_size = 5 # default: GEMSTACK_MAX_THREADS (one per Puma thread)
|
|
28
28
|
# config.db.statement_timeout = 5_000 # ms
|
|
29
29
|
<% end -%>
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Database connection (docs/database.md) — the same format as Rails' database.yml.
|
|
2
|
+
# DATABASE_URL (TEST_DATABASE_URL in tests) overrides this file when set, which
|
|
3
|
+
# is what most hosting platforms provide. Adapters: sqlite3, postgresql, mysql2,
|
|
4
|
+
# trilogy (add the matching gem: sqlite3, pg, mysql2, trilogy).
|
|
5
|
+
default: &default
|
|
6
|
+
<% if database_adapter == "sqlite3" -%>
|
|
7
|
+
adapter: sqlite3
|
|
8
|
+
pool: <%%= ENV.fetch("GEMSTACK_MAX_THREADS", 5) %>
|
|
9
|
+
|
|
10
|
+
development:
|
|
11
|
+
<<: *default
|
|
12
|
+
database: db/development.sqlite3
|
|
13
|
+
|
|
14
|
+
test:
|
|
15
|
+
<<: *default
|
|
16
|
+
database: db/test.sqlite3
|
|
17
|
+
|
|
18
|
+
# Keep the file on persistent storage (a volume); or set DATABASE_URL=sqlite3:/data/app.sqlite3.
|
|
19
|
+
production:
|
|
20
|
+
<<: *default
|
|
21
|
+
database: db/production.sqlite3
|
|
22
|
+
<% elsif database_adapter == "postgresql" -%>
|
|
23
|
+
adapter: postgresql
|
|
24
|
+
pool: <%%= ENV.fetch("GEMSTACK_MAX_THREADS", 5) %>
|
|
25
|
+
# Without host/username, the local server is used as your OS user.
|
|
26
|
+
# host: localhost
|
|
27
|
+
# username: <%= database_name %>
|
|
28
|
+
# password: <%%= ENV["DATABASE_PASSWORD"] %>
|
|
29
|
+
|
|
30
|
+
development:
|
|
31
|
+
<<: *default
|
|
32
|
+
database: <%= database_name %>_development
|
|
33
|
+
|
|
34
|
+
test:
|
|
35
|
+
<<: *default
|
|
36
|
+
database: <%= database_name %>_test
|
|
37
|
+
|
|
38
|
+
# In production, DATABASE_URL (postgres://user:password@host:5432/name) usually replaces this.
|
|
39
|
+
production:
|
|
40
|
+
<<: *default
|
|
41
|
+
database: <%= database_name %>_production
|
|
42
|
+
<% else -%>
|
|
43
|
+
adapter: <%= database_adapter %>
|
|
44
|
+
pool: <%%= ENV.fetch("GEMSTACK_MAX_THREADS", 5) %>
|
|
45
|
+
host: <%%= ENV.fetch("DATABASE_HOST", "127.0.0.1") %>
|
|
46
|
+
username: root
|
|
47
|
+
password: <%%= ENV["DATABASE_PASSWORD"] %>
|
|
48
|
+
|
|
49
|
+
development:
|
|
50
|
+
<<: *default
|
|
51
|
+
database: <%= database_name %>_development
|
|
52
|
+
|
|
53
|
+
test:
|
|
54
|
+
<<: *default
|
|
55
|
+
database: <%= database_name %>_test
|
|
56
|
+
|
|
57
|
+
# In production, DATABASE_URL (<%= database_adapter %>://user:password@host:3306/name) usually replaces this.
|
|
58
|
+
production:
|
|
59
|
+
<<: *default
|
|
60
|
+
database: <%= database_name %>_production
|
|
61
|
+
<% end -%>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Copy to .env for local development overrides. Never commit real secrets.
|
|
2
|
+
# Variables already set in your shell always take precedence.
|
|
3
|
+
|
|
4
|
+
# PORT=3000 # public development port (gemstack dev)
|
|
5
|
+
# GEMSTACK_LOG_LEVEL=debug
|
|
6
|
+
|
|
7
|
+
<% if database? -%>
|
|
8
|
+
# Database: config/database.yml. A URL here overrides it (hosting platforms set DATABASE_URL).
|
|
9
|
+
<% if database_adapter == "sqlite3" -%>
|
|
10
|
+
# DATABASE_URL=sqlite3:db/development.sqlite3
|
|
11
|
+
<% elsif database_adapter == "postgresql" -%>
|
|
12
|
+
# DATABASE_URL=postgres://user:password@localhost:5432/<%= database_name %>_development
|
|
13
|
+
# TEST_DATABASE_URL=postgres://user:password@localhost:5432/<%= database_name %>_test
|
|
14
|
+
<% else -%>
|
|
15
|
+
# DATABASE_PASSWORD=
|
|
16
|
+
# DATABASE_URL=<%= database_adapter %>://root:password@127.0.0.1:3306/<%= database_name %>_development
|
|
17
|
+
# TEST_DATABASE_URL=<%= database_adapter %>://root:password@127.0.0.1:3306/<%= database_name %>_test
|
|
18
|
+
<% end -%>
|
|
19
|
+
<% end -%>
|
data/templates/app/dot_gitignore
CHANGED
|
@@ -5,8 +5,8 @@ Sequel.migration do
|
|
|
5
5
|
change do
|
|
6
6
|
create_table(:users) do
|
|
7
7
|
primary_key :id, type: :Bignum
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
String :email, null: false, unique: true # stored lowercased
|
|
9
|
+
String :password_digest, null: false # Argon2id
|
|
10
10
|
column :email_verified_at, :timestamptz
|
|
11
11
|
column :created_at, :timestamptz, null: false
|
|
12
12
|
column :updated_at, :timestamptz, null: false
|
|
@@ -17,9 +17,9 @@ Sequel.migration do
|
|
|
17
17
|
create_table(:sessions) do
|
|
18
18
|
primary_key :id, type: :Bignum
|
|
19
19
|
foreign_key :user_id, :users, type: :Bignum, null: false, on_delete: :cascade, index: true
|
|
20
|
-
|
|
20
|
+
String :token_digest, size: 64, null: false, unique: true
|
|
21
21
|
column :ip, :inet
|
|
22
|
-
|
|
22
|
+
String :user_agent
|
|
23
23
|
column :created_at, :timestamptz, null: false
|
|
24
24
|
column :last_seen_at, :timestamptz, null: false
|
|
25
25
|
column :expires_at, :timestamptz, null: false, index: true
|
|
@@ -29,10 +29,10 @@ Sequel.migration do
|
|
|
29
29
|
create_table(:auth_tokens) do
|
|
30
30
|
primary_key :id, type: :Bignum
|
|
31
31
|
foreign_key :user_id, :users, type: :Bignum, null: false, on_delete: :cascade, index: true
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
32
|
+
String :purpose, size: 30, null: false
|
|
33
|
+
String :token_digest, size: 64, null: false, unique: true
|
|
34
|
+
String :name
|
|
35
|
+
String :email
|
|
36
36
|
column :created_at, :timestamptz, null: false
|
|
37
37
|
column :last_used_at, :timestamptz
|
|
38
38
|
column :expires_at, :timestamptz
|
|
@@ -14,7 +14,7 @@ ARG NODE_VERSION=<%= node_version %>
|
|
|
14
14
|
|
|
15
15
|
# ── Ruby API ────────────────────────────────────────────────────────────────
|
|
16
16
|
FROM ruby:${RUBY_VERSION}-slim AS api-build
|
|
17
|
-
RUN apt-get update -qq && apt-get install --no-install-recommends -y
|
|
17
|
+
RUN apt-get update -qq && apt-get install --no-install-recommends -y <%= build_packages %> \
|
|
18
18
|
&& rm -rf /var/lib/apt/lists/*
|
|
19
19
|
WORKDIR /app
|
|
20
20
|
ENV BUNDLE_DEPLOYMENT=1 BUNDLE_WITHOUT="development:test" BUNDLE_PATH=/usr/local/bundle
|
|
@@ -25,9 +25,10 @@ COPY . .
|
|
|
25
25
|
RUN rm -rf frontend tmp/* log/* .env*
|
|
26
26
|
|
|
27
27
|
FROM ruby:${RUBY_VERSION}-slim AS api
|
|
28
|
-
RUN apt-get update -qq && apt-get install --no-install-recommends -y
|
|
28
|
+
RUN apt-get update -qq && apt-get install --no-install-recommends -y <%= runtime_packages %> \
|
|
29
29
|
&& rm -rf /var/lib/apt/lists/* \
|
|
30
|
-
&& groupadd --system --gid 1000 app && useradd --system --uid 1000 --gid app --create-home app
|
|
30
|
+
&& groupadd --system --gid 1000 app && useradd --system --uid 1000 --gid app --create-home app<% if sqlite? %> \
|
|
31
|
+
&& mkdir -p /data && chown app:app /data<% end %>
|
|
31
32
|
WORKDIR /app
|
|
32
33
|
ENV GEMSTACK_ENV=production BUNDLE_DEPLOYMENT=1 BUNDLE_WITHOUT="development:test" BUNDLE_PATH=/usr/local/bundle \
|
|
33
34
|
GEMSTACK_API_HOST=0.0.0.0 GEMSTACK_API_PORT=4000
|
|
@@ -1,16 +1,25 @@
|
|
|
1
1
|
# Runs <%= app_name %> in production mode on this machine, behind Caddy on https://localhost:
|
|
2
2
|
#
|
|
3
|
-
# export SECRET_KEY_BASE=$(openssl rand -hex 64) POSTGRES_PASSWORD=$(openssl rand -hex 16)
|
|
3
|
+
# export SECRET_KEY_BASE=$(openssl rand -hex 64)<% if postgresql? %> POSTGRES_PASSWORD=$(openssl rand -hex 16)<% elsif mysql? %> MYSQL_PASSWORD=$(openssl rand -hex 16)<% end %>
|
|
4
4
|
# docker compose up --build
|
|
5
5
|
#
|
|
6
6
|
# It's also a map of what a real deploy needs: one image per process type,
|
|
7
7
|
# migrations before the API starts, and a proxy sending /api to Ruby.
|
|
8
|
+
<% if sqlite? -%>
|
|
9
|
+
# SQLite: the database file lives on the `data` volume, shared by the API, the
|
|
10
|
+
# jobs worker and migrations — keep them on one host.
|
|
11
|
+
<% end -%>
|
|
8
12
|
name: <%= app_name.tr("_", "-") %>
|
|
9
13
|
|
|
10
14
|
x-api-env: &api-env
|
|
11
15
|
GEMSTACK_ENV: production
|
|
12
16
|
SECRET_KEY_BASE: ${SECRET_KEY_BASE:?set SECRET_KEY_BASE (openssl rand -hex 64)}
|
|
13
|
-
|
|
17
|
+
<% if database? -%>
|
|
18
|
+
DATABASE_URL: <%= database_url %>
|
|
19
|
+
<% end -%>
|
|
20
|
+
<% if redis? -%>
|
|
21
|
+
REDIS_URL: redis://redis:6379/0 # realtime fan-out between the API and the jobs worker
|
|
22
|
+
<% end -%>
|
|
14
23
|
APP_URL: ${APP_URL:-https://localhost}
|
|
15
24
|
<% if mail? -%>
|
|
16
25
|
SMTP_URL: ${SMTP_URL:-} # e.g. smtp://user:pass@smtp.example.com:587 — emails retry until it is set
|
|
@@ -18,6 +27,7 @@ x-api-env: &api-env
|
|
|
18
27
|
<% end -%>
|
|
19
28
|
|
|
20
29
|
services:
|
|
30
|
+
<% if postgresql? -%>
|
|
21
31
|
db:
|
|
22
32
|
image: postgres:17
|
|
23
33
|
environment:
|
|
@@ -30,21 +40,53 @@ services:
|
|
|
30
40
|
interval: 2s
|
|
31
41
|
retries: 30
|
|
32
42
|
|
|
43
|
+
<% elsif mysql? -%>
|
|
44
|
+
db:
|
|
45
|
+
image: mysql:8.4
|
|
46
|
+
environment:
|
|
47
|
+
MYSQL_DATABASE: app
|
|
48
|
+
MYSQL_USER: app
|
|
49
|
+
MYSQL_PASSWORD: ${MYSQL_PASSWORD:?set MYSQL_PASSWORD}
|
|
50
|
+
MYSQL_RANDOM_ROOT_PASSWORD: "yes"
|
|
51
|
+
volumes: [db:/var/lib/mysql]
|
|
52
|
+
healthcheck:
|
|
53
|
+
test: ["CMD-SHELL", "mysqladmin ping -h 127.0.0.1 -uapp -p$$MYSQL_PASSWORD --silent"]
|
|
54
|
+
interval: 3s
|
|
55
|
+
retries: 40
|
|
56
|
+
|
|
57
|
+
<% end -%>
|
|
58
|
+
<% if redis? -%>
|
|
59
|
+
redis:
|
|
60
|
+
image: redis:7
|
|
61
|
+
command: ["redis-server", "--save", "", "--appendonly", "no"]
|
|
62
|
+
|
|
63
|
+
<% end -%>
|
|
64
|
+
<% if database? -%>
|
|
33
65
|
migrate:
|
|
34
66
|
image: <%= app_name %>-api
|
|
35
67
|
build: { context: ., target: api }
|
|
36
68
|
healthcheck: { disable: true }
|
|
37
69
|
command: ["bundle", "exec", "gemstack", "db:migrate"]
|
|
38
70
|
environment: *api-env
|
|
71
|
+
<% if sqlite? -%>
|
|
72
|
+
volumes: [data:/data]
|
|
73
|
+
<% else -%>
|
|
39
74
|
depends_on: { db: { condition: service_healthy } }
|
|
75
|
+
<% end -%>
|
|
40
76
|
|
|
77
|
+
<% end -%>
|
|
41
78
|
api:
|
|
42
79
|
image: <%= app_name %>-api
|
|
43
80
|
build: { context: ., target: api }
|
|
44
81
|
environment:
|
|
45
82
|
<<: *api-env
|
|
46
83
|
WEB_CONCURRENCY: ${WEB_CONCURRENCY:-2}
|
|
84
|
+
<% if sqlite? -%>
|
|
85
|
+
volumes: [data:/data]
|
|
86
|
+
<% end -%>
|
|
87
|
+
<% if database? -%>
|
|
47
88
|
depends_on: { migrate: { condition: service_completed_successfully } }
|
|
89
|
+
<% end -%>
|
|
48
90
|
<% if jobs? -%>
|
|
49
91
|
|
|
50
92
|
jobs:
|
|
@@ -53,6 +95,9 @@ services:
|
|
|
53
95
|
healthcheck: { disable: true }
|
|
54
96
|
command: ["bundle", "exec", "gemstack", "jobs"]
|
|
55
97
|
environment: *api-env
|
|
98
|
+
<% if sqlite? -%>
|
|
99
|
+
volumes: [data:/data]
|
|
100
|
+
<% end -%>
|
|
56
101
|
depends_on: { migrate: { condition: service_completed_successfully } }
|
|
57
102
|
<% end -%>
|
|
58
103
|
<% if frontend? -%>
|
|
@@ -76,5 +121,9 @@ services:
|
|
|
76
121
|
depends_on: [api<%= ", web" if frontend? %>]
|
|
77
122
|
|
|
78
123
|
volumes:
|
|
124
|
+
<% if sqlite? -%>
|
|
125
|
+
data:
|
|
126
|
+
<% elsif database? -%>
|
|
79
127
|
db:
|
|
128
|
+
<% end -%>
|
|
80
129
|
caddy:
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: gemstack-cli
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Shoaib Malik
|
|
@@ -15,28 +15,28 @@ dependencies:
|
|
|
15
15
|
requirements:
|
|
16
16
|
- - '='
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version: 0.
|
|
18
|
+
version: 0.2.0
|
|
19
19
|
type: :runtime
|
|
20
20
|
prerelease: false
|
|
21
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
22
|
requirements:
|
|
23
23
|
- - '='
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
|
-
version: 0.
|
|
25
|
+
version: 0.2.0
|
|
26
26
|
- !ruby/object:Gem::Dependency
|
|
27
27
|
name: gemstack-dev
|
|
28
28
|
requirement: !ruby/object:Gem::Requirement
|
|
29
29
|
requirements:
|
|
30
30
|
- - '='
|
|
31
31
|
- !ruby/object:Gem::Version
|
|
32
|
-
version: 0.
|
|
32
|
+
version: 0.2.0
|
|
33
33
|
type: :runtime
|
|
34
34
|
prerelease: false
|
|
35
35
|
version_requirements: !ruby/object:Gem::Requirement
|
|
36
36
|
requirements:
|
|
37
37
|
- - '='
|
|
38
38
|
- !ruby/object:Gem::Version
|
|
39
|
-
version: 0.
|
|
39
|
+
version: 0.2.0
|
|
40
40
|
- !ruby/object:Gem::Dependency
|
|
41
41
|
name: thor
|
|
42
42
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -83,6 +83,7 @@ files:
|
|
|
83
83
|
- templates/app/bin/gemstack
|
|
84
84
|
- templates/app/config.ru
|
|
85
85
|
- templates/app/config/app.rb.tt
|
|
86
|
+
- templates/app/config/database.yml.tt
|
|
86
87
|
- templates/app/config/environments/development.rb
|
|
87
88
|
- templates/app/config/environments/production.rb
|
|
88
89
|
- templates/app/config/environments/test.rb
|
|
@@ -90,7 +91,7 @@ files:
|
|
|
90
91
|
- templates/app/config/routes.rb
|
|
91
92
|
- templates/app/db/migrations/dot_keep
|
|
92
93
|
- templates/app/db/seeds.rb
|
|
93
|
-
- templates/app/dot_env.example
|
|
94
|
+
- templates/app/dot_env.example.tt
|
|
94
95
|
- templates/app/dot_gitignore
|
|
95
96
|
- templates/app/dot_ruby-version.tt
|
|
96
97
|
- templates/app/dot_tool-versions.tt
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
# Copy to .env for local development overrides. Never commit real secrets.
|
|
2
|
-
# Variables already set in your shell always take precedence.
|
|
3
|
-
|
|
4
|
-
# PORT=3000 # public development port (gemstack dev)
|
|
5
|
-
# GEMSTACK_LOG_LEVEL=debug
|
|
6
|
-
|
|
7
|
-
# PostgreSQL. Defaults: postgres:///<app>_development locally (current OS user).
|
|
8
|
-
# DATABASE_URL=postgres://user:password@localhost:5432/myapp_development
|
|
9
|
-
# TEST_DATABASE_URL=postgres://user:password@localhost:5432/myapp_test
|