kuby-core 0.8.0 → 0.8.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +8 -0
- data/Gemfile +1 -0
- data/lib/kuby.rb +2 -0
- data/lib/kuby/docker/metadata.rb +14 -12
- data/lib/kuby/docker/timestamp_tag.rb +6 -0
- data/lib/kuby/plugins/rails_app/tasks.rake +2 -2
- data/lib/kuby/version.rb +1 -1
- data/spec/docker/metadata_spec.rb +192 -0
- data/spec/docker/timestamp_tag_spec.rb +54 -4
- data/spec/dummy/Gemfile +54 -0
- data/spec/dummy/Gemfile.lock +223 -0
- data/spec/dummy/README.md +24 -0
- data/spec/dummy/Rakefile +6 -0
- data/spec/dummy/app/assets/config/manifest.js +2 -0
- data/spec/dummy/app/assets/stylesheets/application.css +15 -0
- data/spec/dummy/app/channels/application_cable/channel.rb +4 -0
- data/spec/dummy/app/channels/application_cable/connection.rb +4 -0
- data/spec/dummy/app/controllers/application_controller.rb +2 -0
- data/spec/dummy/app/helpers/application_helper.rb +2 -0
- data/spec/dummy/app/javascript/channels/consumer.js +6 -0
- data/spec/dummy/app/javascript/channels/index.js +5 -0
- data/spec/dummy/app/javascript/packs/application.js +17 -0
- data/spec/dummy/app/jobs/application_job.rb +7 -0
- data/spec/dummy/app/mailers/application_mailer.rb +4 -0
- data/spec/dummy/app/models/application_record.rb +3 -0
- data/spec/dummy/app/views/layouts/application.html.erb +15 -0
- data/spec/dummy/app/views/layouts/mailer.html.erb +13 -0
- data/spec/dummy/app/views/layouts/mailer.text.erb +1 -0
- data/spec/dummy/bin/bundle +114 -0
- data/spec/dummy/bin/rails +9 -0
- data/spec/dummy/bin/rake +9 -0
- data/spec/dummy/bin/setup +36 -0
- data/spec/dummy/bin/spring +17 -0
- data/spec/dummy/bin/yarn +11 -0
- data/spec/dummy/config.ru +5 -0
- data/spec/dummy/config/application.rb +19 -0
- data/spec/dummy/config/boot.rb +4 -0
- data/spec/dummy/config/cable.yml +10 -0
- data/spec/dummy/config/credentials.yml.enc +1 -0
- data/spec/dummy/config/database.yml +25 -0
- data/spec/dummy/config/environment.rb +5 -0
- data/spec/dummy/config/environments/development.rb +62 -0
- data/spec/dummy/config/environments/production.rb +112 -0
- data/spec/dummy/config/environments/test.rb +49 -0
- data/spec/dummy/config/initializers/application_controller_renderer.rb +8 -0
- data/spec/dummy/config/initializers/assets.rb +14 -0
- data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/spec/dummy/config/initializers/content_security_policy.rb +30 -0
- data/spec/dummy/config/initializers/cookies_serializer.rb +5 -0
- data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
- data/spec/dummy/config/initializers/inflections.rb +16 -0
- data/spec/dummy/config/initializers/mime_types.rb +4 -0
- data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/spec/dummy/config/locales/en.yml +33 -0
- data/spec/dummy/config/master.key +1 -0
- data/spec/dummy/config/puma.rb +38 -0
- data/spec/dummy/config/routes.rb +3 -0
- data/spec/dummy/config/spring.rb +6 -0
- data/spec/dummy/config/storage.yml +34 -0
- data/spec/dummy/db/seeds.rb +7 -0
- data/spec/dummy/package.json +11 -0
- data/spec/dummy/public/404.html +67 -0
- data/spec/dummy/public/422.html +67 -0
- data/spec/dummy/public/500.html +66 -0
- data/spec/dummy/public/apple-touch-icon-precomposed.png +0 -0
- data/spec/dummy/public/apple-touch-icon.png +0 -0
- data/spec/dummy/public/favicon.ico +0 -0
- data/spec/dummy/public/robots.txt +1 -0
- data/spec/dummy/test/application_system_test_case.rb +5 -0
- data/spec/dummy/test/channels/application_cable/connection_test.rb +11 -0
- data/spec/dummy/test/test_helper.rb +13 -0
- data/spec/dummy/tmp/cache/bootsnap-load-path-cache +0 -0
- data/spec/spec_helper.rb +70 -2
- data/spec/support/docker/fake_cli.rb +54 -0
- data/spec/support/docker/remote/fake_client.rb +16 -0
- data/spec/trailing_hash_spec.rb +23 -0
- metadata +69 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a0f3610dd2dc149fb09e589c56dc477534ece66a752106b251a94ac265cb1e34
|
4
|
+
data.tar.gz: 673285ecde402c63a461ef4c4374a1353be45ff33610db840590658cc6cab5ed
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6c8e407acf3a35f21b4446b0c0752a754720a7fbd03b887b64c4fe0991e61e6f107f61a2adb2394ac88d24470be5c814c6168cd96106295c777f50a35f77820a
|
7
|
+
data.tar.gz: 1871572ccb876d772eec874ed224235905ba7265ced27319f4e7abd678a65e1862584bd88f0223e2f87ba1b99158342e6c6a2c4d033d758b28338296153f5e4b
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,11 @@
|
|
1
|
+
## 0.8.1
|
2
|
+
* Fix database config rewriter task.
|
3
|
+
- Broke with refactoring of database config code.
|
4
|
+
* More correctly parse Docker image URLs.
|
5
|
+
- It can be challenging to identify the hostname in image URLs because 1) the host can be omitted, and 2) the scheme is often omitted.
|
6
|
+
- The new strategy is to look for a "." in the first segment of the URL if there is no scheme. It's not bulletproof but is better than what we had before, which was to assume the first segment was the host. Eg. for an image URL like camertron/foo, we would identify the host as "camertron."
|
7
|
+
* Added a number of tests and a Rails dummy app in spec/.
|
8
|
+
|
1
9
|
## 0.8.0
|
2
10
|
* Upgrade to Krane >= 1.1.4, < 2.0.
|
3
11
|
* Remove Krane monkeypatch in ext/.
|
data/Gemfile
CHANGED
data/lib/kuby.rb
CHANGED
data/lib/kuby/docker/metadata.rb
CHANGED
@@ -5,6 +5,7 @@ module Kuby
|
|
5
5
|
class Metadata
|
6
6
|
DEFAULT_DISTRO = :debian
|
7
7
|
DEFAULT_REGISTRY_HOST = 'https://www.docker.com'.freeze
|
8
|
+
DEFAULT_REGISTRY_SCHEME = 'https'
|
8
9
|
LATEST_TAG = 'latest'
|
9
10
|
|
10
11
|
attr_accessor :image_url
|
@@ -20,12 +21,7 @@ module Kuby
|
|
20
21
|
end
|
21
22
|
|
22
23
|
def image_host
|
23
|
-
@image_host ||=
|
24
|
-
uri = parse_url(image_url)
|
25
|
-
"#{uri.scheme}://#{uri.host}"
|
26
|
-
else
|
27
|
-
DEFAULT_REGISTRY_HOST
|
28
|
-
end
|
24
|
+
@image_host ||= "#{full_image_uri.scheme}://#{full_image_uri.host}"
|
29
25
|
end
|
30
26
|
|
31
27
|
def image_hostname
|
@@ -33,11 +29,7 @@ module Kuby
|
|
33
29
|
end
|
34
30
|
|
35
31
|
def image_repo
|
36
|
-
@image_repo ||=
|
37
|
-
parse_url(image_url).path.sub(/\A\//, '')
|
38
|
-
else
|
39
|
-
image_url
|
40
|
-
end
|
32
|
+
@image_repo ||= full_image_uri.path.sub(/\A[\/]+/, '')
|
41
33
|
end
|
42
34
|
|
43
35
|
def tags
|
@@ -76,6 +68,16 @@ module Kuby
|
|
76
68
|
|
77
69
|
private
|
78
70
|
|
71
|
+
def full_image_uri
|
72
|
+
@full_image_uri ||= if image_url.include?('://')
|
73
|
+
URI.parse(image_url)
|
74
|
+
elsif image_url =~ /\A[^.]+\.[^\/]+\//
|
75
|
+
URI.parse("#{DEFAULT_REGISTRY_SCHEME}://#{image_url}")
|
76
|
+
else
|
77
|
+
URI.parse("#{DEFAULT_REGISTRY_HOST}/#{image_url.sub(/\A[\/]+/, '')}")
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
79
81
|
def default_image_url
|
80
82
|
# assuming dockerhub by not specifying full url
|
81
83
|
@default_image_url ||= environment.app_name.downcase
|
@@ -92,7 +94,7 @@ module Kuby
|
|
92
94
|
return uri if uri.scheme
|
93
95
|
|
94
96
|
# force a scheme because URI.parse won't work properly without one
|
95
|
-
URI.parse("
|
97
|
+
URI.parse("#{DEFAULT_REGISTRY_SCHEME}://#{url}")
|
96
98
|
end
|
97
99
|
end
|
98
100
|
end
|
@@ -9,8 +9,8 @@ namespace :kuby do
|
|
9
9
|
config_file = File.join(Kuby.environment.kubernetes.plugin(:rails_app).root, 'config', 'database.yml')
|
10
10
|
database = Kuby.environment.kubernetes.plugin(:rails_app).database
|
11
11
|
|
12
|
-
if database.respond_to?(:rewritten_configs)
|
13
|
-
File.write(config_file, YAML.dump(database.rewritten_configs))
|
12
|
+
if database.plugin.respond_to?(:rewritten_configs)
|
13
|
+
File.write(config_file, YAML.dump(database.plugin.rewritten_configs))
|
14
14
|
Kuby.logger.info("Wrote #{config_file}")
|
15
15
|
end
|
16
16
|
end
|
data/lib/kuby/version.rb
CHANGED
@@ -0,0 +1,192 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'timecop'
|
3
|
+
|
4
|
+
describe Kuby::Docker::Metadata do
|
5
|
+
let(:metadata) { definition.environment.docker.metadata }
|
6
|
+
|
7
|
+
describe '#image_url' do
|
8
|
+
subject { metadata.image_url }
|
9
|
+
|
10
|
+
it { is_expected.to eq(docker_image_url) }
|
11
|
+
|
12
|
+
context 'when no image URL is configured' do
|
13
|
+
let(:docker_image_url) { nil }
|
14
|
+
it { is_expected.to eq(definition.app_name) }
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
describe '#image_host' do
|
19
|
+
subject { metadata.image_host }
|
20
|
+
|
21
|
+
it { is_expected.to eq(described_class::DEFAULT_REGISTRY_HOST) }
|
22
|
+
|
23
|
+
context 'when the image URL contains an explicit host' do
|
24
|
+
let(:docker_image_url) { 'registry.foo.com/foo/testapp' }
|
25
|
+
|
26
|
+
it { is_expected.to eq('https://registry.foo.com') }
|
27
|
+
end
|
28
|
+
|
29
|
+
context 'when the image URL contains an explicit host with scheme' do
|
30
|
+
let(:docker_image_url) { 'http://registry.foo.com/foo/testapp' }
|
31
|
+
|
32
|
+
it { is_expected.to eq('http://registry.foo.com') }
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
describe '#image_repo' do
|
37
|
+
subject { metadata.image_repo }
|
38
|
+
|
39
|
+
it { is_expected.to eq('foo/testapp') }
|
40
|
+
|
41
|
+
context 'when the image URL contains an explicit host' do
|
42
|
+
let(:docker_image_url) { 'registry.foo.com/foo/testapp' }
|
43
|
+
|
44
|
+
it { is_expected.to eq('foo/testapp') }
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
describe '#image_hostname' do
|
49
|
+
subject { metadata.image_hostname }
|
50
|
+
|
51
|
+
it { is_expected.to eq('www.docker.com') }
|
52
|
+
|
53
|
+
context 'when the image URL contains an explicit host' do
|
54
|
+
let(:docker_image_url) { 'registry.foo.com/foo/testapp' }
|
55
|
+
|
56
|
+
it { is_expected.to eq('registry.foo.com') }
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
describe '#tags' do
|
61
|
+
subject { metadata.tags }
|
62
|
+
|
63
|
+
it 'specifies the current timestamp tag and the default tag' do
|
64
|
+
Timecop.freeze do
|
65
|
+
expect(subject).to eq([
|
66
|
+
Time.now.strftime('%Y%m%d%H%M%S'),
|
67
|
+
Kuby::Docker::Metadata::LATEST_TAG
|
68
|
+
])
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
describe '#tag' do
|
74
|
+
let(:tag) { make_ts_tag(Time.now) }
|
75
|
+
|
76
|
+
subject { metadata.tag }
|
77
|
+
|
78
|
+
context 'with no local or remote tags' do
|
79
|
+
it 'raises an error' do
|
80
|
+
expect { subject }.to raise_error(Kuby::Docker::MissingTagError)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
context 'with an available remote tag' do
|
85
|
+
before { docker_remote_client.tags << tag }
|
86
|
+
|
87
|
+
it { is_expected.to eq(tag) }
|
88
|
+
end
|
89
|
+
|
90
|
+
context 'with an available local tag' do
|
91
|
+
before do
|
92
|
+
docker_cli.build(
|
93
|
+
dockerfile: nil,
|
94
|
+
image_url: docker_image_url,
|
95
|
+
tags: [tag]
|
96
|
+
)
|
97
|
+
end
|
98
|
+
|
99
|
+
it { is_expected.to eq(tag) }
|
100
|
+
end
|
101
|
+
|
102
|
+
context 'with multiple remote tags' do
|
103
|
+
let(:time) { Time.now }
|
104
|
+
|
105
|
+
before do
|
106
|
+
docker_remote_client.tags +=
|
107
|
+
[time - 5, time + 10, time - 10, time + 15].map do |t|
|
108
|
+
make_ts_tag(t)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
it { is_expected.to eq(make_ts_tag(time + 15)) }
|
113
|
+
end
|
114
|
+
|
115
|
+
context 'with multiple local and remote tags' do
|
116
|
+
let(:time) { Time.now }
|
117
|
+
|
118
|
+
before do
|
119
|
+
docker_remote_client.tags +=
|
120
|
+
[time - 5, time + 10, time - 10, time + 15].map do |t|
|
121
|
+
make_ts_tag(t)
|
122
|
+
end
|
123
|
+
|
124
|
+
docker_cli.build(
|
125
|
+
dockerfile: nil,
|
126
|
+
image_url: docker_image_url,
|
127
|
+
tags: [time - 3, time + 6, time - 6, time + 18].map do |t|
|
128
|
+
make_ts_tag(t)
|
129
|
+
end
|
130
|
+
)
|
131
|
+
end
|
132
|
+
|
133
|
+
it { is_expected.to eq(make_ts_tag(time + 18)) }
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
describe '#previous_tag' do
|
138
|
+
let(:time) { Time.now }
|
139
|
+
let(:current_tag) { make_ts_tag(time) }
|
140
|
+
|
141
|
+
before do
|
142
|
+
docker_remote_client.tags << current_tag
|
143
|
+
docker_cli.build(
|
144
|
+
dockerfile: nil,
|
145
|
+
image_url: docker_image_url,
|
146
|
+
tags: [current_tag]
|
147
|
+
)
|
148
|
+
end
|
149
|
+
|
150
|
+
subject { metadata.previous_tag(current_tag) }
|
151
|
+
|
152
|
+
context 'with no previous local or remote tag' do
|
153
|
+
it 'raises an error' do
|
154
|
+
expect { subject }.to raise_error(Kuby::Docker::MissingTagError)
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
context 'with an available previous remote tag' do
|
159
|
+
let(:previous_tag) { make_ts_tag(time - 5) }
|
160
|
+
|
161
|
+
before { docker_remote_client.tags << previous_tag }
|
162
|
+
|
163
|
+
it { is_expected.to eq(previous_tag) }
|
164
|
+
end
|
165
|
+
|
166
|
+
context 'with an available previous local tag' do
|
167
|
+
let(:previous_tag) { make_ts_tag(time - 5) }
|
168
|
+
|
169
|
+
before do
|
170
|
+
docker_cli.build(
|
171
|
+
dockerfile: nil,
|
172
|
+
image_url: docker_image_url,
|
173
|
+
tags: [previous_tag]
|
174
|
+
)
|
175
|
+
end
|
176
|
+
|
177
|
+
it { is_expected.to eq(previous_tag) }
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
describe '#distro' do
|
182
|
+
subject { metadata.distro }
|
183
|
+
|
184
|
+
it { is_expected.to eq(Kuby::Docker::Metadata::DEFAULT_DISTRO) }
|
185
|
+
|
186
|
+
context 'with a distro set manually' do
|
187
|
+
before { metadata.distro = :alpine }
|
188
|
+
|
189
|
+
it { is_expected.to eq(:alpine) }
|
190
|
+
end
|
191
|
+
end
|
192
|
+
end
|
@@ -1,11 +1,61 @@
|
|
1
1
|
require "spec_helper"
|
2
2
|
|
3
3
|
describe Kuby::Docker::TimestampTag do
|
4
|
-
context
|
5
|
-
|
6
|
-
tag = described_class.try_parse("20200810165134")
|
4
|
+
context '.try_parse' do
|
5
|
+
let(:tag_str) { '20200810165134' }
|
7
6
|
|
8
|
-
|
7
|
+
it 'creates a new timestamp tag' do
|
8
|
+
tag = described_class.try_parse(tag_str)
|
9
|
+
expect(tag).to be_a(described_class)
|
10
|
+
end
|
11
|
+
|
12
|
+
it 'correctly parses the timestamp contained in the tag' do
|
13
|
+
time = described_class.try_parse(tag_str).time
|
14
|
+
expect([time.year, time.month, time.day, time.hour, time.min, time.sec]).to(
|
15
|
+
eq([2020, 8, 10, 16, 51, 34])
|
16
|
+
)
|
17
|
+
end
|
18
|
+
|
19
|
+
context 'with an invalid tag' do
|
20
|
+
let(:tag_str) { 'abc123' }
|
21
|
+
|
22
|
+
it 'returns nil' do
|
23
|
+
expect(described_class.try_parse(tag_str)).to eq(nil)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
context '#to_s' do
|
29
|
+
it 'serializes the tag as a timestamp' do
|
30
|
+
tag = described_class.new(Time.new(2020, 8, 10, 16, 51, 34))
|
31
|
+
expect(tag.to_s).to eq('20200810165134')
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
context 'comparison' do
|
36
|
+
it 'ensures tags can be compared by their timestamp values' do
|
37
|
+
seed_time = Time.now
|
38
|
+
times = [seed_time, seed_time + 5, seed_time + 10, seed_time + 15].shuffle
|
39
|
+
tags = times.map { |t| described_class.new(t) }
|
40
|
+
expect(tags.sort.map(&:time)).to eq(times.sort)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
context 'equality' do
|
45
|
+
it 'ensures tags with equal times are considered equal' do
|
46
|
+
time = Time.now
|
47
|
+
tag1 = described_class.new(time)
|
48
|
+
tag2 = described_class.new(time)
|
49
|
+
expect(tag1).to eq(tag2)
|
50
|
+
expect(tag1.hash).to eq(tag2.hash)
|
51
|
+
end
|
52
|
+
|
53
|
+
it 'ensures tags with inequal times are not considered equal' do
|
54
|
+
time = Time.now
|
55
|
+
tag1 = described_class.new(time)
|
56
|
+
tag2 = described_class.new(time + 5)
|
57
|
+
expect(tag1).to_not eq(tag2)
|
58
|
+
expect(tag1.hash).to_not eq(tag2.hash)
|
9
59
|
end
|
10
60
|
end
|
11
61
|
end
|
data/spec/dummy/Gemfile
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
source 'https://rubygems.org'
|
2
|
+
git_source(:github) { |repo| "https://github.com/#{repo}.git" }
|
3
|
+
|
4
|
+
ruby '2.5.8'
|
5
|
+
|
6
|
+
# Bundle edge Rails instead: gem 'rails', github: 'rails/rails'
|
7
|
+
gem 'rails', '~> 6.0.3', '>= 6.0.3.2'
|
8
|
+
# Use sqlite3 as the database for Active Record
|
9
|
+
gem 'sqlite3', '~> 1.4'
|
10
|
+
# Use Puma as the app server
|
11
|
+
gem 'puma', '~> 4.1'
|
12
|
+
# Use SCSS for stylesheets
|
13
|
+
gem 'sass-rails', '>= 6'
|
14
|
+
# Transpile app-like JavaScript. Read more: https://github.com/rails/webpacker
|
15
|
+
gem 'webpacker', '~> 4.0'
|
16
|
+
# Turbolinks makes navigating your web application faster. Read more: https://github.com/turbolinks/turbolinks
|
17
|
+
gem 'turbolinks', '~> 5'
|
18
|
+
# Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder
|
19
|
+
gem 'jbuilder', '~> 2.7'
|
20
|
+
# Use Redis adapter to run Action Cable in production
|
21
|
+
# gem 'redis', '~> 4.0'
|
22
|
+
# Use Active Model has_secure_password
|
23
|
+
# gem 'bcrypt', '~> 3.1.7'
|
24
|
+
|
25
|
+
# Use Active Storage variant
|
26
|
+
# gem 'image_processing', '~> 1.2'
|
27
|
+
|
28
|
+
# Reduces boot times through caching; required in config/boot.rb
|
29
|
+
gem 'bootsnap', '>= 1.4.2', require: false
|
30
|
+
|
31
|
+
group :development, :test do
|
32
|
+
# Call 'byebug' anywhere in the code to stop execution and get a debugger console
|
33
|
+
gem 'byebug', platforms: [:mri, :mingw, :x64_mingw]
|
34
|
+
end
|
35
|
+
|
36
|
+
group :development do
|
37
|
+
# Access an interactive console on exception pages or by calling 'console' anywhere in the code.
|
38
|
+
gem 'web-console', '>= 3.3.0'
|
39
|
+
gem 'listen', '~> 3.2'
|
40
|
+
# Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring
|
41
|
+
gem 'spring'
|
42
|
+
gem 'spring-watcher-listen', '~> 2.0.0'
|
43
|
+
end
|
44
|
+
|
45
|
+
group :test do
|
46
|
+
# Adds support for Capybara system testing and selenium driver
|
47
|
+
gem 'capybara', '>= 2.15'
|
48
|
+
gem 'selenium-webdriver'
|
49
|
+
# Easy installation and use of web drivers to run system tests with browsers
|
50
|
+
gem 'webdrivers'
|
51
|
+
end
|
52
|
+
|
53
|
+
# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
|
54
|
+
gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby]
|