gemstack 0.4.2 → 0.4.3
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/commands/deploy.rb +78 -0
- data/lib/gemstack/cli/deploy_files.rb +125 -0
- data/lib/gemstack/cli/deploy_generator.rb +14 -2
- data/lib/gemstack/cli/destroy_generator.rb +2 -1
- data/lib/gemstack/cli/generation_manifest.rb +8 -0
- data/lib/gemstack/cli/template_update.rb +12 -2
- data/lib/gemstack/cli.rb +4 -2
- data/lib/gemstack/version.rb +1 -1
- data/templates/deploy/bin/deploy +9 -0
- data/templates/deploy/config/deploy.yml.tt +24 -7
- data/templates/deploy/dot_kamal/secrets.tt +7 -2
- metadata +6 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6310feb0f5388f4664b5d3cc6a628487dae761765a4b8ee8c09c36e3096da3e8
|
|
4
|
+
data.tar.gz: ae643ed6d28ef9403ccc15b351adb77fddc3cb8e2573ffb27a75dd6207549a2f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d4b0d241f841248a9ffd8e9fa0947e60114299ae8d01e72cee926a48aec24e469fe47ff1dc00f9d75043453e4e90a898c2a728065fcdac1d57a89db596489e1a
|
|
7
|
+
data.tar.gz: 1c5f793e4228c3182bffb2162bf8d94ff38fc01e1aaea6b9fde397977cfcbf15d1905f1d9f962e7412a052023b8d8102fc39bba98eb92eae4eec1ad6653931b8
|
data/CHANGELOG.md
CHANGED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
|
|
5
|
+
module GemStack
|
|
6
|
+
class CLI < Thor
|
|
7
|
+
# `gemstack deploy [KAMAL ARGS]` — Kamal with the app's root .env loaded,
|
|
8
|
+
# so .kamal/secrets ($SECRET_KEY_BASE, $DATABASE_URL, …) and the frontend's
|
|
9
|
+
# NEXT_PUBLIC_* build values come from the same file as everything else.
|
|
10
|
+
# Kamal doesn't read .env itself.
|
|
11
|
+
class DeployCommand
|
|
12
|
+
# Production values first; .env.local is left out on purpose: on a
|
|
13
|
+
# developer's machine it holds development overrides (a local
|
|
14
|
+
# DATABASE_URL…) that must never reach production.
|
|
15
|
+
ENV_FILES = %w[.env.production.local .env.production .env].freeze
|
|
16
|
+
# Kamal builds the image from a clone of the last commit: uncommitted
|
|
17
|
+
# changes wouldn't be deployed.
|
|
18
|
+
BUILDS = %w[deploy setup redeploy build].freeze
|
|
19
|
+
|
|
20
|
+
def initialize(root:, env: ENV, output: $stderr, exec: ->(*command, **opts) { Kernel.exec(*command, **opts) })
|
|
21
|
+
@root = root
|
|
22
|
+
@env = env
|
|
23
|
+
@output = output
|
|
24
|
+
@exec = exec
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def run(args)
|
|
28
|
+
unless File.file?(File.join(@root, "config/deploy.yml"))
|
|
29
|
+
raise Thor::Error, "No config/deploy.yml: run `gemstack generate deploy` first (docs/deployment.md)"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
args = ["deploy"] if args.empty?
|
|
33
|
+
refuse_uncommitted_changes! if BUILDS.include?(args.first)
|
|
34
|
+
load_env_files
|
|
35
|
+
@exec.call("bundle", "exec", "kamal", *args, chdir: @root)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def load_env_files
|
|
41
|
+
files = ENV_FILES.map { |name| File.join(@root, name) }.select { |path| File.file?(path) }
|
|
42
|
+
if files.empty?
|
|
43
|
+
return @output.puts("gemstack deploy: no .env.production or .env; .kamal/secrets reads the environment")
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
loaded = Dotenv.load(files, env: @env)
|
|
47
|
+
names = files.map { |path| File.basename(path) }.join(", ")
|
|
48
|
+
@output.puts("gemstack deploy: #{loaded.size} settings from #{names} (the environment wins)")
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def refuse_uncommitted_changes!
|
|
52
|
+
changes, status = Open3.capture2("git", "-C", @root, "status", "--porcelain")
|
|
53
|
+
return unless status.success? && !changes.strip.empty?
|
|
54
|
+
|
|
55
|
+
raise Thor::Error, "Uncommitted changes: Kamal builds the image from the last commit, so they wouldn't " \
|
|
56
|
+
"be deployed. Commit (or stash) them first."
|
|
57
|
+
rescue SystemCallError
|
|
58
|
+
nil # no git: nothing to check
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
desc "deploy [KAMAL ARGS]", "Deploy with Kamal, the root .env.production/.env loaded (or any Kamal command)"
|
|
63
|
+
long_desc <<~DESC
|
|
64
|
+
Runs `bundle exec kamal` with the app's root .env files loaded (.env.production.local,
|
|
65
|
+
.env.production, .env — earlier files win, real environment variables win over all), so
|
|
66
|
+
.kamal/secrets resolves its $VARIABLES from them. Without arguments it runs `kamal deploy`,
|
|
67
|
+
and refuses when there are uncommitted changes (Kamal builds from the last commit).
|
|
68
|
+
|
|
69
|
+
gemstack deploy # build, push and deploy the last commit
|
|
70
|
+
gemstack deploy setup # the first deploy: Docker, kamal-proxy, accessories
|
|
71
|
+
gemstack deploy logs # any Kamal command or alias: console, migrate, rollback VERSION…
|
|
72
|
+
DESC
|
|
73
|
+
stop_on_unknown_option! :deploy
|
|
74
|
+
def deploy(*args)
|
|
75
|
+
DeployCommand.new(root: Project.root!).run(args)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
|
|
5
|
+
module GemStack
|
|
6
|
+
class CLI < Thor
|
|
7
|
+
# The deploy half of `gemstack update --templates`: the files
|
|
8
|
+
# `gemstack generate deploy` wrote (Dockerfile, config/deploy.yml,
|
|
9
|
+
# .kamal/secrets, bin/…). .gemstack/generators.json records each one's
|
|
10
|
+
# content as generated: a file that still matches was never edited and is
|
|
11
|
+
# updated; for one you edited, the release whose render matches is the base
|
|
12
|
+
# the merge starts from. Files a release adds (bin/deploy) are created.
|
|
13
|
+
module DeployFiles
|
|
14
|
+
DEPLOY_OWNER = "deploy:app"
|
|
15
|
+
# What DeployGenerator reads to decide what to write (frontend? database? jobs? mail?).
|
|
16
|
+
DEPLOY_INPUTS = %w[Gemfile Gemfile.lock config/app.rb config/database.yml .ruby-version .tool-versions
|
|
17
|
+
frontend/package.json frontend/next.config.ts].freeze
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def update_deploy_files
|
|
22
|
+
@manifest = GenerationManifest.new(@root)
|
|
23
|
+
tracked = @manifest.files.select { |_, entry| entry["owner"] == DEPLOY_OWNER }
|
|
24
|
+
return unless tracked.any? || File.file?(File.join(@root, "config/deploy.yml"))
|
|
25
|
+
|
|
26
|
+
Dir.mktmpdir("gemstack-deploy-templates") do |cache|
|
|
27
|
+
@deploy_cache = cache
|
|
28
|
+
tree = render_deploy(Generator::TEMPLATES)
|
|
29
|
+
@output.puts(" Deploy files: couldn't render this version's deploy templates; skipped") unless tree
|
|
30
|
+
tree&.each { |path, file| apply_deploy(path, file, tracked[path]) }
|
|
31
|
+
end
|
|
32
|
+
@manifest.save unless @dry_run
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def apply_deploy(path, file, entry)
|
|
36
|
+
mine_path = File.join(@root, path)
|
|
37
|
+
mine = File.binread(mine_path) if File.file?(mine_path)
|
|
38
|
+
return if mine == file.content
|
|
39
|
+
return (entry ? @result.removed << path : record_deploy(path, file) { create(path, file) }) if mine.nil?
|
|
40
|
+
if entry && Digest::SHA256.hexdigest(mine) == entry["sha256"] # never edited
|
|
41
|
+
return record_deploy(path, file) { update(path, file, :updated) }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
base = deploy_base(path, entry)
|
|
45
|
+
return if base == file.content # this release didn't change it: your edits are yours
|
|
46
|
+
|
|
47
|
+
bases = base ? [base] : deploy_bases(path)
|
|
48
|
+
reason = "is new in this version, and you have your own" if entry.nil? && bases.empty?
|
|
49
|
+
record_deploy(path, file) { conflict(path, file, mine, bases, **{ reason: reason }.compact) }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Runs the write, then records what the file now derives from: the new
|
|
53
|
+
# template, unless you kept yours (skip / FILE.new).
|
|
54
|
+
def record_deploy(path, file)
|
|
55
|
+
yield
|
|
56
|
+
written = (@result.created + @result.updated + @result.merged + @result.marked).include?(path)
|
|
57
|
+
@manifest.record_content(path, owner: DEPLOY_OWNER, content: file.content) if written && !@dry_run
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# The template this file was generated from: the release render whose
|
|
61
|
+
# content matches the recorded checksum.
|
|
62
|
+
def deploy_base(path, entry)
|
|
63
|
+
return nil unless entry
|
|
64
|
+
|
|
65
|
+
deploy_releases.each do |source|
|
|
66
|
+
content = deploy_render(source)&.dig(path)&.content
|
|
67
|
+
return content if content && Digest::SHA256.hexdigest(content) == entry["sha256"]
|
|
68
|
+
end
|
|
69
|
+
nil
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Without a recorded base: every earlier release's version of the file
|
|
73
|
+
# (this release's isn't a base — merging with it would just keep yours).
|
|
74
|
+
def deploy_bases(path)
|
|
75
|
+
deploy_releases.drop(1).filter_map { |source| deploy_render(source)&.dig(path)&.content }.uniq
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# This release (what a merged file was recorded against), the recorded
|
|
79
|
+
# version, then every earlier release, newest first.
|
|
80
|
+
def deploy_releases
|
|
81
|
+
@deploy_releases ||= [Generator::TEMPLATES] +
|
|
82
|
+
if @old_templates then Array(@old_templates)
|
|
83
|
+
else ([@from] + TemplateSource.releases_before(GemStack::VERSION).reverse).compact.uniq
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def deploy_render(source)
|
|
88
|
+
(@deploy_renders ||= {}).fetch(source) do
|
|
89
|
+
templates = File.directory?(source.to_s) ? source : TemplateSource.locate(source, cache: @deploy_cache)
|
|
90
|
+
@deploy_renders[source] = templates && render_deploy(templates)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# `gemstack generate deploy` for this app (its database, frontend, jobs,
|
|
95
|
+
# pinned versions and any lib/templates/gemstack/deploy overrides), in a
|
|
96
|
+
# scratch copy of the files it reads.
|
|
97
|
+
def render_deploy(templates)
|
|
98
|
+
Dir.mktmpdir("gemstack-render-deploy") do |dir|
|
|
99
|
+
sandbox = File.join(dir, File.basename(@root))
|
|
100
|
+
copy_deploy_inputs(sandbox)
|
|
101
|
+
generator = DeployGenerator.new(root: sandbox, output: StringIO.new, templates: templates)
|
|
102
|
+
generator.run
|
|
103
|
+
generator.created.filter_map do |full|
|
|
104
|
+
rel = full.delete_prefix("#{sandbox}/")
|
|
105
|
+
[rel, TemplateUpdate::AppFile.new(File.binread(full), File.stat(full).mode)] unless rel == "vendor/.keep"
|
|
106
|
+
end.to_h
|
|
107
|
+
end
|
|
108
|
+
rescue StandardError
|
|
109
|
+
nil # a release whose deploy templates this version can't render: compare without it
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def copy_deploy_inputs(sandbox)
|
|
113
|
+
globs = DEPLOY_INPUTS + %w[app/jobs/*.rb app/mailers/*.rb lib/templates/gemstack/deploy/**/*]
|
|
114
|
+
globs.flat_map { |glob| Dir.glob(glob, File::FNM_DOTMATCH, base: @root) }.each do |rel|
|
|
115
|
+
source = File.join(@root, rel)
|
|
116
|
+
next unless File.file?(source)
|
|
117
|
+
|
|
118
|
+
FileUtils.mkdir_p(File.dirname(File.join(sandbox, rel)))
|
|
119
|
+
FileUtils.cp(source, File.join(sandbox, rel))
|
|
120
|
+
end
|
|
121
|
+
File.write(File.join(sandbox, "Gemfile"), "") unless File.file?(File.join(sandbox, "Gemfile"))
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
@@ -7,11 +7,18 @@ module GemStack
|
|
|
7
7
|
# to Ruby and handles HTTPS; database accessories), .kamal/secrets,
|
|
8
8
|
# bin/docker-entrypoint and .dockerignore. Nothing is deployed anywhere.
|
|
9
9
|
class DeployGenerator < Generator
|
|
10
|
-
|
|
10
|
+
# templates: another release's templates/ directory (gemstack update
|
|
11
|
+
# renders old releases' deploy files to compare with).
|
|
12
|
+
def initialize(root:, output: $stdout, force: false, templates: nil)
|
|
11
13
|
super(output: output, force: force)
|
|
12
14
|
@root = root
|
|
15
|
+
@templates = templates
|
|
13
16
|
end
|
|
14
17
|
|
|
18
|
+
# Bundler 4 lockfiles list gem checksums; gems installed from the local
|
|
19
|
+
# cache can be added without one, so fill them in before building.
|
|
20
|
+
CHECKSUMS_HINT = ", then bundle lock --add-checksums (Gemfile.lock lists gem checksums)"
|
|
21
|
+
|
|
15
22
|
KAMAL_GEM = %(gem "kamal", require: false, group: :development # bundle exec kamal deploy (config/deploy.yml)\n)
|
|
16
23
|
# The image runs Next.js's standalone server (node -r … frontend/server.js); the
|
|
17
24
|
# Dockerfile asks for that build with GEMSTACK_NEXT_OUTPUT=standalone.
|
|
@@ -103,7 +110,12 @@ module GemStack
|
|
|
103
110
|
return status("identical", path, "kamal") if gemfile.match?(/^\s*gem ["']kamal["']/)
|
|
104
111
|
|
|
105
112
|
File.write(path, "#{gemfile.chomp}\n\n#{KAMAL_GEM}")
|
|
106
|
-
status("gem", path, "kamal — run bundle install")
|
|
113
|
+
status("gem", path, "kamal — run bundle install#{CHECKSUMS_HINT if lockfile_checksums?}")
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def lockfile_checksums?
|
|
117
|
+
lock = File.join(@root, "Gemfile.lock")
|
|
118
|
+
File.file?(lock) && File.read(lock).match?(/^CHECKSUMS$/)
|
|
107
119
|
end
|
|
108
120
|
|
|
109
121
|
def enable_standalone_frontend
|
|
@@ -14,7 +14,8 @@ module GemStack
|
|
|
14
14
|
# Kinds that change the API, so the TypeScript contract is regenerated after them.
|
|
15
15
|
CONTRACT_KINDS = %w[model controller resource].freeze
|
|
16
16
|
# What `gemstack generate deploy` writes at the app's root.
|
|
17
|
-
DEPLOY_FILES = %w[Dockerfile .dockerignore config/deploy.yml .kamal/secrets bin/docker-entrypoint
|
|
17
|
+
DEPLOY_FILES = %w[Dockerfile .dockerignore config/deploy.yml .kamal/secrets bin/docker-entrypoint
|
|
18
|
+
bin/deploy].freeze
|
|
18
19
|
SHARED = (Generator::BASE_CLASSES.values + %w[
|
|
19
20
|
app/controllers/application_controller.rb test/test_helper.rb
|
|
20
21
|
frontend/lib/format.ts frontend/lib/gemstack/client.ts
|
|
@@ -56,6 +56,14 @@ module GemStack
|
|
|
56
56
|
@dirty = true
|
|
57
57
|
end
|
|
58
58
|
|
|
59
|
+
# Records a file as generated from `content` — what a later update
|
|
60
|
+
# compares with — whatever the file holds now (e.g. after a merge).
|
|
61
|
+
def record_content(relative, owner:, content:)
|
|
62
|
+
absolute(relative)
|
|
63
|
+
files[relative] = { "owner" => owner, "sha256" => Digest::SHA256.hexdigest(content) }
|
|
64
|
+
@dirty = true
|
|
65
|
+
end
|
|
66
|
+
|
|
59
67
|
def record_route(line, owner:)
|
|
60
68
|
entry = { "owner" => owner, "line" => line }
|
|
61
69
|
return if routes.include?(entry)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "digest"
|
|
3
4
|
require "fileutils"
|
|
4
5
|
require "json"
|
|
5
6
|
require "net/http"
|
|
@@ -82,11 +83,19 @@ module GemStack
|
|
|
82
83
|
# template are applied to your copy, keeping your edits. Where both changed
|
|
83
84
|
# the same lines the file gets <<<<<<< conflict markers to resolve.
|
|
84
85
|
#
|
|
86
|
+
# The files `gemstack generate deploy` wrote (Dockerfile, config/deploy.yml,
|
|
87
|
+
# .kamal/secrets, bin/…) are updated the same way. .gemstack/generators.json
|
|
88
|
+
# records each one's content as generated: a file that still matches was
|
|
89
|
+
# never edited; for one you edited, the release whose render matches is the
|
|
90
|
+
# base the merge starts from. Files a release adds (bin/deploy) are created.
|
|
91
|
+
#
|
|
85
92
|
# Apps record the templates they're on in .gemstack/version. Older apps don't,
|
|
86
93
|
# so each file is compared with every earlier release's template: matching
|
|
87
94
|
# one means you never edited it (it's just out of date). Without any old
|
|
88
95
|
# templates (offline), files that differ are offered, never updated.
|
|
89
96
|
class TemplateUpdate
|
|
97
|
+
include DeployFiles
|
|
98
|
+
|
|
90
99
|
VERSION_FILE = ".gemstack/version"
|
|
91
100
|
# Managed elsewhere: the Gemfile by `gemstack update`/Bundler. Migrations
|
|
92
101
|
# (db/migrations/) are the app's history and never part of an update.
|
|
@@ -121,6 +130,7 @@ module GemStack
|
|
|
121
130
|
new_tree = render(Generator::TEMPLATES, version: GemStack::VERSION)
|
|
122
131
|
old_trees = old_trees_for_comparison
|
|
123
132
|
new_tree.each { |path, file| apply(path, file, old_trees.map { |tree| tree[path]&.content }) }
|
|
133
|
+
update_deploy_files
|
|
124
134
|
report
|
|
125
135
|
record_version unless @dry_run || @result.conflicts.any?
|
|
126
136
|
@result
|
|
@@ -230,11 +240,11 @@ module GemStack
|
|
|
230
240
|
status("update", path)
|
|
231
241
|
end
|
|
232
242
|
|
|
233
|
-
def conflict(path, file, mine, bases)
|
|
243
|
+
def conflict(path, file, mine, bases, reason: "changed in this version and you edited it")
|
|
234
244
|
return record_conflict(path) unless @interactive
|
|
235
245
|
|
|
236
246
|
loop do
|
|
237
|
-
case @prompt.call("#{path}
|
|
247
|
+
case @prompt.call("#{path} #{reason} — [m]erge (keeps your edits), " \
|
|
238
248
|
"[o]verwrite, [s]kip, [d]iff, save as [n]ew file?")
|
|
239
249
|
when "m"
|
|
240
250
|
return if merge(path, file, mine, bases)
|
data/lib/gemstack/cli.rb
CHANGED
|
@@ -25,9 +25,11 @@ module GemStack
|
|
|
25
25
|
require_relative "cli/deploy_generator"
|
|
26
26
|
require_relative "cli/add_generator"
|
|
27
27
|
require_relative "cli/update_generator"
|
|
28
|
+
require_relative "cli/deploy_files"
|
|
28
29
|
require_relative "cli/template_update"
|
|
29
30
|
require_relative "cli/commands/db"
|
|
30
31
|
require_relative "cli/commands/jobs"
|
|
32
|
+
require_relative "cli/commands/deploy"
|
|
31
33
|
require_relative "cli/console_methods"
|
|
32
34
|
require_relative "cli/helpers"
|
|
33
35
|
|
|
@@ -238,10 +240,10 @@ module GemStack
|
|
|
238
240
|
"~> VERSION" and runs bundle update for them. Without VERSION, uses the latest release on
|
|
239
241
|
rubygems.org. Then updates the files `gemstack new` wrote (config, bin/, frontend setup…):
|
|
240
242
|
files the release didn't change are left alone, new ones are created, ones you never edited
|
|
241
|
-
are updated, and for ones you edited you choose: overwrite, skip, see the diff, or FILE.new.
|
|
243
|
+
are updated, and for ones you edited you choose: merge, overwrite, skip, see the diff, or FILE.new.
|
|
242
244
|
|
|
243
245
|
gemstack update # the latest release
|
|
244
|
-
gemstack update 0.4.
|
|
246
|
+
gemstack update 0.4.3 # a specific version
|
|
245
247
|
gemstack update --templates --dry-run # only the template step, preview
|
|
246
248
|
gemstack update --templates --from 0.3.5
|
|
247
249
|
DESC
|
data/lib/gemstack/version.rb
CHANGED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# Deploys with Kamal, the root .env.production / .env loaded (docs/deployment.md):
|
|
3
|
+
#
|
|
4
|
+
# bin/deploy # build, push and deploy the last commit
|
|
5
|
+
# bin/deploy setup # the first deploy: Docker, kamal-proxy, accessories
|
|
6
|
+
# bin/deploy logs # any Kamal command or alias: console, migrate, rollback VERSION…
|
|
7
|
+
set -e
|
|
8
|
+
cd "$(dirname "$0")/.."
|
|
9
|
+
exec bundle exec gemstack deploy "$@"
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
# Kamal deploy configuration for <%= app_name %> (gemstack generate deploy — docs/deployment.md).
|
|
2
2
|
#
|
|
3
|
-
#
|
|
4
|
-
#
|
|
3
|
+
# bin/deploy setup # first deploy: Docker, kamal-proxy (HTTPS) and accessories on the server
|
|
4
|
+
# bin/deploy # every deploy after that (the last commit), with zero downtime
|
|
5
|
+
# bin/deploy logs # any Kamal command or alias below: console, migrate, rollback VERSION…
|
|
5
6
|
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
7
|
+
# bin/deploy (gemstack deploy) runs Kamal with the root .env.production and .env
|
|
8
|
+
# loaded. Replace the placeholders marked CHANGE. Secrets are read from
|
|
9
|
+
# .kamal/secrets, which only references those variables, never their values.
|
|
8
10
|
|
|
9
11
|
service: <%= service %>
|
|
10
12
|
|
|
@@ -17,6 +19,8 @@ servers:
|
|
|
17
19
|
hosts:
|
|
18
20
|
- 192.168.0.1 # CHANGE: your server's IP or hostname
|
|
19
21
|
cmd: node -r ./frontend/lib/gemstack/root-env.cjs frontend/server.js
|
|
22
|
+
# Not the primary role (see primary_role below), so the proxy is enabled explicitly.
|
|
23
|
+
proxy: true
|
|
20
24
|
env:
|
|
21
25
|
clear:
|
|
22
26
|
# Server Components call the API directly, through the alias below.
|
|
@@ -57,12 +61,21 @@ servers:
|
|
|
57
61
|
cmd: bundle exec gemstack jobs
|
|
58
62
|
<% end -%>
|
|
59
63
|
|
|
60
|
-
|
|
64
|
+
<% if frontend? -%>
|
|
65
|
+
# The API boots first: the web role's healthcheck (/) renders pages that call it,
|
|
66
|
+
# which fails on the first deploy if the web role boots before any API is running.
|
|
67
|
+
<% end -%>
|
|
68
|
+
primary_role: api
|
|
61
69
|
|
|
62
70
|
# HTTPS with a Let's Encrypt certificate: point the domain's DNS at the server first.
|
|
63
71
|
proxy:
|
|
64
72
|
ssl: true
|
|
65
73
|
host: app.example.com # CHANGE: your domain
|
|
74
|
+
# Serving www and the bare domain: list both (each gets a certificate) and
|
|
75
|
+
# redirect one to the other in frontend/next.config.ts (docs/deployment.md).
|
|
76
|
+
# hosts:
|
|
77
|
+
# - www.example.com
|
|
78
|
+
# - example.com
|
|
66
79
|
<% if frontend? -%>
|
|
67
80
|
app_port: 3000
|
|
68
81
|
healthcheck:
|
|
@@ -74,7 +87,7 @@ proxy:
|
|
|
74
87
|
<% end -%>
|
|
75
88
|
|
|
76
89
|
registry:
|
|
77
|
-
# server: ghcr.io # for GitHub's registry; Docker Hub is the default
|
|
90
|
+
# server: ghcr.io # for GitHub's registry (password: see .kamal/secrets); Docker Hub is the default
|
|
78
91
|
username: your-user # CHANGE
|
|
79
92
|
password:
|
|
80
93
|
- KAMAL_REGISTRY_PASSWORD
|
|
@@ -109,6 +122,10 @@ volumes:
|
|
|
109
122
|
- "<%= service %>_data:/data"
|
|
110
123
|
<% end -%>
|
|
111
124
|
|
|
125
|
+
# Kamal connects as root; set the user your server logs in with (e.g. ubuntu on AWS).
|
|
126
|
+
# ssh:
|
|
127
|
+
# user: ubuntu
|
|
128
|
+
|
|
112
129
|
builder:
|
|
113
130
|
arch: amd64
|
|
114
131
|
<% if frontend? -%>
|
|
@@ -153,7 +170,7 @@ accessories:
|
|
|
153
170
|
<% end -%>
|
|
154
171
|
<% end -%>
|
|
155
172
|
|
|
156
|
-
#
|
|
173
|
+
# bin/deploy console | logs | migrate | shell
|
|
157
174
|
aliases:
|
|
158
175
|
console: app exec --interactive --reuse --roles=api "bundle exec gemstack console"
|
|
159
176
|
logs: app logs --follow
|
|
@@ -3,12 +3,17 @@
|
|
|
3
3
|
# This file is committed to git: only ever write REFERENCES here — environment
|
|
4
4
|
# variables ($NAME) or password-manager commands ($(kamal secrets fetch ...)) —
|
|
5
5
|
# never a secret's value. `gemstack doctor` flags values written in place.
|
|
6
|
+
#
|
|
7
|
+
# bin/deploy (gemstack deploy) loads the root .env.production.local, .env.production
|
|
8
|
+
# and .env (git-ignored) before Kamal reads this file: put the production values there.
|
|
6
9
|
|
|
10
|
+
# Docker Hub: an access token. GitHub's registry (ghcr.io): a token with write:packages,
|
|
11
|
+
# e.g. KAMAL_REGISTRY_PASSWORD=$(gh auth token) after `gh auth refresh -s write:packages`.
|
|
7
12
|
KAMAL_REGISTRY_PASSWORD=$KAMAL_REGISTRY_PASSWORD
|
|
8
13
|
<% if frontend? -%>
|
|
9
|
-
# The frontend's NEXT_PUBLIC_* settings from the root .env (
|
|
14
|
+
# The frontend's NEXT_PUBLIC_* settings from the root .env files (production first),
|
|
10
15
|
# compiled into the JavaScript at build time — public by design, nothing else is read.
|
|
11
|
-
GEMSTACK_PUBLIC_ENV=$(grep -hsE '^(export )?NEXT_PUBLIC_' .env.production .env)
|
|
16
|
+
GEMSTACK_PUBLIC_ENV=$(grep -hsE '^(export )?NEXT_PUBLIC_' .env.production.local .env.production .env)
|
|
12
17
|
<% end -%>
|
|
13
18
|
SECRET_KEY_BASE=$SECRET_KEY_BASE
|
|
14
19
|
<% if postgresql? -%>
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: gemstack
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.4.
|
|
4
|
+
version: 0.4.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Adware Technologies
|
|
@@ -44,14 +44,14 @@ dependencies:
|
|
|
44
44
|
requirements:
|
|
45
45
|
- - '='
|
|
46
46
|
- !ruby/object:Gem::Version
|
|
47
|
-
version: 0.4.
|
|
47
|
+
version: 0.4.3
|
|
48
48
|
type: :runtime
|
|
49
49
|
prerelease: false
|
|
50
50
|
version_requirements: !ruby/object:Gem::Requirement
|
|
51
51
|
requirements:
|
|
52
52
|
- - '='
|
|
53
53
|
- !ruby/object:Gem::Version
|
|
54
|
-
version: 0.4.
|
|
54
|
+
version: 0.4.3
|
|
55
55
|
- !ruby/object:Gem::Dependency
|
|
56
56
|
name: json
|
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -175,9 +175,11 @@ files:
|
|
|
175
175
|
- lib/gemstack/cli/add_next_steps.rb
|
|
176
176
|
- lib/gemstack/cli/app_generator.rb
|
|
177
177
|
- lib/gemstack/cli/commands/db.rb
|
|
178
|
+
- lib/gemstack/cli/commands/deploy.rb
|
|
178
179
|
- lib/gemstack/cli/commands/jobs.rb
|
|
179
180
|
- lib/gemstack/cli/console_methods.rb
|
|
180
181
|
- lib/gemstack/cli/controller_generator.rb
|
|
182
|
+
- lib/gemstack/cli/deploy_files.rb
|
|
181
183
|
- lib/gemstack/cli/deploy_generator.rb
|
|
182
184
|
- lib/gemstack/cli/destroy_generator.rb
|
|
183
185
|
- lib/gemstack/cli/destroy_generator/kinds.rb
|
|
@@ -336,6 +338,7 @@ files:
|
|
|
336
338
|
- templates/controller/app/controllers/%file_name%_controller.rb.tt
|
|
337
339
|
- templates/controller/test/controllers/%file_name%_controller_test.rb.tt
|
|
338
340
|
- templates/deploy/Dockerfile.tt
|
|
341
|
+
- templates/deploy/bin/deploy
|
|
339
342
|
- templates/deploy/bin/docker-entrypoint.tt
|
|
340
343
|
- templates/deploy/config/deploy.yml.tt
|
|
341
344
|
- templates/deploy/dot_dockerignore
|