consist 0.1.0 → 0.1.2

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.
data/README.md CHANGED
@@ -2,41 +2,656 @@
2
2
 
3
3
  [![Gem Version](https://img.shields.io/gem/v/consist)](https://rubygems.org/gems/consist)
4
4
  [![Gem Downloads](https://img.shields.io/gem/dt/consist)](https://www.ruby-toolbox.com/projects/consist)
5
- [![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/johnmcdowall/consist/ci.yml)](https://github.com/johnmcdowall/consist/actions/workflows/ci.yml)
6
- [![Code Climate maintainability](https://img.shields.io/codeclimate/maintainability/johnmcdowall/consist)](https://codeclimate.com/github/johnmcdowall/consist)
5
+ [![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/consist-sh/consist/ci.yml)](https://github.com/consist-sh/consist/actions/workflows/ci.yml)
6
+ [![Code Climate maintainability](https://img.shields.io/codeclimate/maintainability/consist-sh/consist)](https://codeclimate.com/github/consist-sh/consist)
7
7
 
8
- TODO: Description of this gem goes here.
8
+ **THIS IS BETA SOFTWARE UNDER ACTIVE DEVELOPMENT. APIs AND FEATURES WILL CHANGE.**
9
+
10
+ > consist - (noun): a set of railroad vehicles forming a complete train.
11
+
12
+ `consist` is the one person framework server scaffolder. It is stone age tech.
13
+
14
+ You can use it to quickly baseline a raw server using a given recipe provided
15
+ by Consist. I use it to baseline new Droplets to be ready to run Kamal in
16
+ single server setup for a Rails monolith. While Kamal will setup Docker
17
+ for you, it does not do anything else related to configuring the underlying
18
+ server, such as firewalls, general hardening, enabling swapfile etc.
19
+
20
+ ## Project Principles
21
+
22
+ - Minimal tool specific language / knowledge required to use Consist
23
+ - Procedural declaration execution - no converging, orchestration or
24
+ event driven operation
25
+ - If you can shell script it, you can `consist` it directly
9
26
 
10
27
  ---
11
28
 
12
29
  - [Quick start](#quick-start)
13
30
  - [Support](#support)
31
+ - [Rationale](#rationale)
32
+ - [Key Concepts](#key-concepts)
33
+ - [Is It Good?](#is-it-good%3F)
14
34
  - [License](#license)
15
35
  - [Code of conduct](#code-of-conduct)
16
36
  - [Contribution guide](#contribution-guide)
17
37
 
18
38
  ## Quick start
19
39
 
20
- ```
40
+ ```sh
21
41
  gem install consist
22
42
  ```
23
43
 
44
+ You must be already auth'd with the server you want to scaffold. `consist` will use
45
+ your SSH id to perform actions.
46
+
47
+ Then, you have two ways of interacting with Consist. First is the `scaffold` command:
48
+
49
+ ```sh
50
+ consist scaffold <recipe_name> root <ip_address>
51
+ ```
52
+
53
+ Will kick off the scaffolding of that given server with the given
54
+ recipe, using the `root` user.
55
+
56
+ The other way of using `consist` is to go with a `Consistfile` in
57
+ your project root that describes the recipe and steps. Then you can say:
58
+
59
+ ```sh
60
+ consist up <ip_address>
61
+ ```
62
+
63
+ And `consist` will do it's thing with that given IP address.
64
+
65
+ ## Features
66
+
67
+ - Simple Ruby based DSL
68
+ - ERB interpolation of config on shell commands and file contents
69
+ - Small API surface area - quick to learn
70
+
71
+ ## Rationale
72
+
73
+ I wanted a super-simple tool, that was baked in Ruby, for setting up
74
+ random servers to specific configurations. This is the result.
75
+
76
+ On a scale of 1 to 10, with 10 being Terraform, this tool is basically
77
+ as low-rent you can get to hand running scripts yourself, so about a 3
78
+ on the scale.
79
+
80
+ If you know how to shell script what you want, you can stick it in a step,
81
+ and add it to a recipe.
82
+
83
+ The more I work in this industry, the less I see using other people's code
84
+ and tools as a benefit, and more of a liability. I appreciate the paradox I'm
85
+ creating here for you 😅
86
+
87
+ ### Why not use Terraform / Ansible / Salt etc?
88
+
89
+ I think they are bad tools for my needs. I wanted something simple
90
+ I could hack on, grow only when needed, and will work specifically
91
+ without ambiguity. For example, Ansible has a lot of nonsense with case sensitivity,
92
+ Terraform does [weird unexpected things](https://github.com/hashicorp/terraform/issues/16330).
93
+
94
+ I didn't want to keep maintaining specific knowledge of these infrastructure
95
+ as code tools in my brain anymore, along with all of their peculiarities and oddities.
96
+
97
+ **If you prefer those tools, go ahead and use them.**
98
+
99
+ Ain't nobody stopping you.
100
+
101
+ ## Key Concepts
102
+
103
+ Consist leans on three primary ideas: recipes, steps and files. Recipes contain
104
+ one or more steps. Steps tend to be atomic and idempotent.
105
+
106
+ ### Recipes
107
+
108
+ Example of a recipe:
109
+
24
110
  ```ruby
25
- require "consist"
111
+ name "Kamal Single Server"
112
+ description "Sets up a single server to run Kamal"
113
+ user :root
114
+
115
+ steps do
116
+ step :update_apt_packages
117
+ step :install_apt_packages
118
+ end
119
+ ```
120
+
121
+ ### Steps
122
+
123
+ Example of a step:
124
+
125
+ ```ruby
126
+ name "Install APT packages"
127
+ required_user :root
128
+
129
+ shell "Installing essential packages" do
130
+ <<~EOS
131
+ apt-get -y remove systemd-timesyncd
132
+ timedatectl set-ntp no
133
+ apt-get -y install build-essential curl fail2ban git ntp vim
134
+ apt-get autoremove
135
+ apt-get autoclean
136
+ EOS
137
+ end
138
+
139
+ shell "Start NTP and Fail2Ban" do
140
+ <<~EOS
141
+ service ntp restart
142
+ service fail2ban restart
143
+ EOS
144
+ end
26
145
  ```
27
146
 
147
+ ### Files
148
+
149
+ Example of a file:
150
+
151
+ ```ruby
152
+ file :hostname do
153
+ <<~EOS
154
+ <%= hostname %>
155
+ EOS
156
+ end
157
+ ```
158
+
159
+ ### Consistfile
160
+
161
+ A `Consistfile` is a portable giant file of a recipe and all its
162
+ steps. Something like (this is a _full_ example, in practice you
163
+ would reference some of Consist's built in steps):
164
+
165
+ ```ruby
166
+ consist do
167
+ config :hostname, "testexample.com"
168
+ config :site_fqdn, "textexample.com"
169
+ config :admin_email, "j@jmd.fm"
170
+ config :swap_size, "2G"
171
+ config :swap_swappiness, "60"
172
+ config :timezone, "UTC"
173
+
174
+ file :apt_auto_upgrade do
175
+ <<~EOS
176
+ APT::Periodic::AutocleanInterval "7";
177
+ APT::Periodic::Update-Package-Lists "1";
178
+ APT::Periodic::Unattended-Upgrade "1";
179
+ EOS
180
+ end
181
+
182
+ file :hostname do
183
+ <<~EOS
184
+ <%= hostname %>
185
+ EOS
186
+ end
187
+
188
+ file :timezone do
189
+ <<~EOS
190
+ <%= timezone %>
191
+ EOS
192
+ end
193
+
194
+ file :fail2ban_config do
195
+ <<~EOS
196
+ # Fail2Ban configuration file.
197
+ #
198
+
199
+ # to view current bans, run one of the following:
200
+ # fail2ban-client status ssh
201
+ # iptables --list -n | fgrep DROP
202
+
203
+ # The DEFAULT allows a global definition of the options. They can be overridden
204
+ # in each jail afterwards.
205
+
206
+ [DEFAULT]
207
+
208
+ ignoreip = 127.0.0.1
209
+ bantime = 600
210
+ maxretry = 3
211
+ backend = auto
212
+ usedns = warn
213
+ destemail = <%= admin_email %>
214
+
215
+ #
216
+ # ACTIONS
217
+ #
218
+
219
+ banaction = iptables-multiport
220
+ mta = sendmail
221
+ protocol = tcp
222
+ chain = INPUT
223
+
224
+ #
225
+ # Action shortcuts. To be used to define action parameter
226
+
227
+ # The simplest action to take: ban only
228
+ action_ = %(banaction)s[name=%(__name__)s, port="%(port)s", protocol="%(protocol)s", chain="%(chain)s"]
229
+
230
+ # ban & send an e-mail with whois report to the destemail.
231
+ action_mw = %(banaction)s[name=%(__name__)s, port="%(port)s", protocol="%(protocol)s", chain="%(chain)s"]
232
+ %(mta)s-whois[name=%(__name__)s, dest="%(destemail)s", protocol="%(protocol)s", chain="%(chain)s"]
233
+
234
+ # ban & send an e-mail with whois report and relevant log lines
235
+ # to the destemail.
236
+ action_mwl = %(banaction)s[name=%(__name__)s, port="%(port)s", protocol="%(protocol)s", chain="%(chain)s"]
237
+ %(mta)s-whois-lines[name=%(__name__)s, dest="%(destemail)s", logpath=%(logpath)s, chain="%(chain)s"]
238
+
239
+ # default action
240
+ action = %(action_mw)s
241
+
242
+ [ssh]
243
+
244
+ enabled = true
245
+ port = 987
246
+ filter = sshd
247
+ logpath = /var/log/auth.log
248
+ maxretry = 6
249
+
250
+ [ssh-ddos]
251
+
252
+ enabled = true
253
+ port = 987
254
+ filter = sshd-ddos
255
+ logpath = /var/log/auth.log
256
+ maxretry = 6
257
+ EOS
258
+ end
259
+
260
+ file :logwatch_config do
261
+ <<~EOS
262
+ Output = mail
263
+ MailTo = <%= admin_email %>
264
+ MailFrom = logwatch@host1.mydomain.org
265
+ Detail = Low
266
+ Service = All
267
+ EOS
268
+ end
269
+
270
+ file :sysctl_config do
271
+ <<~EOS
272
+ # Do not accept ICMP redirects (prevent MITM attacks)
273
+ net.ipv4.conf.all.accept_redirects = 0
274
+ net.ipv6.conf.all.accept_redirects = 0
275
+ # Do not send ICMP redirects (we are not a router)
276
+ net.ipv4.conf.all.send_redirects = 0
277
+ # Log Martian Packets
278
+ net.ipv4.conf.all.log_martians = 1
279
+ # Controls IP packet forwarding
280
+ net.ipv4.ip_forward = 0
281
+ # Controls source route verification
282
+ net.ipv4.conf.default.rp_filter = 1
283
+ # Do not accept source routing
284
+ net.ipv4.conf.default.accept_source_route = 0
285
+ # Controls the System Request debugging functionality of the kernel
286
+ kernel.sysrq = 0
287
+ # Controls whether core dumps will append the PID to the core filename
288
+ # Useful for debugging multi-threaded applications
289
+ kernel.core_uses_pid = 1
290
+ # Controls the use of TCP syncookies
291
+ net.ipv4.tcp_synack_retries = 2
292
+ ######## IPv4 networking start ###########
293
+ # Send redirects, if router, but this is just server
294
+ net.ipv4.conf.all.send_redirects = 0
295
+ net.ipv4.conf.default.send_redirects = 0
296
+ # Accept packets with SRR option? No
297
+ net.ipv4.conf.all.accept_source_route = 0
298
+ # Accept Redirects? No, this is not router
299
+ net.ipv4.conf.all.accept_redirects = 0
300
+ net.ipv4.conf.all.secure_redirects = 0
301
+ # Log packets with impossible addresses to kernel log? Yes
302
+ net.ipv4.conf.all.log_martians = 1
303
+ net.ipv4.conf.default.accept_source_route = 0
304
+ net.ipv4.conf.default.accept_redirects = 0
305
+ net.ipv4.conf.default.secure_redirects = 0
306
+ # Ignore all ICMP ECHO and TIMESTAMP requests sent to it via broadcast/multicast
307
+ net.ipv4.icmp_echo_ignore_broadcasts = 1
308
+ # Prevent against the common 'syn flood attack'
309
+ net.ipv4.tcp_syncookies = 1
310
+ # Enable source validation by reversed path, as specified in RFC1812
311
+ net.ipv4.conf.all.rp_filter = 1
312
+ net.ipv4.conf.default.rp_filter = 1
313
+ ######## IPv6 networking start ###########
314
+ # Number of Router Solicitations to send until assuming no routers are present.
315
+ # This is host and not router
316
+ net.ipv6.conf.default.router_solicitations = 0
317
+ # Accept Router Preference in RA?
318
+ net.ipv6.conf.default.accept_ra_rtr_pref = 0
319
+ # Learn Prefix Information in Router Advertisement
320
+ net.ipv6.conf.default.accept_ra_pinfo = 0
321
+ # Setting controls whether the system will accept Hop Limit settings from a router advertisement
322
+ net.ipv6.conf.default.accept_ra_defrtr = 0
323
+ #router advertisements can cause the system to assign a global unicast address to an interface
324
+ net.ipv6.conf.default.autoconf = 0
325
+ #how many neighbor solicitations to send out per address?
326
+ net.ipv6.conf.default.dad_transmits = 0
327
+ # How many global unicast IPv6 addresses can be assigned to each interface?
328
+ net.ipv6.conf.default.max_addresses = 1
329
+ ######## IPv6 networking ends ###########
330
+ # Disabled, not used anymore
331
+ #Enable ExecShield protection |
332
+ #kernel.exec-shield = 1
333
+ #kernel.randomize_va_space = 1
334
+ # TCP and memory optimization
335
+ # increase TCP max buffer size setable using setsockopt()
336
+ #net.ipv4.tcp_rmem = 4096 87380 8388608
337
+ #net.ipv4.tcp_wmem = 4096 87380 8388608
338
+ # increase Linux auto tuning TCP buffer limits
339
+ #net.core.rmem_max = 8388608
340
+ #net.core.wmem_max = 8388608
341
+ #net.core.netdev_max_backlog = 5000
342
+ #net.ipv4.tcp_window_scaling = 1
343
+ # increase system file descriptor limit
344
+ fs.file-max = 65535
345
+ #Allow for more PIDs
346
+ kernel.pid_max = 65536
347
+ #Increase system IP port limits
348
+ net.ipv4.ip_local_port_range = 2000 65000
349
+ # Disable IPv6 autoconf
350
+ #net.ipv6.conf.all.autoconf = 0
351
+ #net.ipv6.conf.default.autoconf = 0
352
+ #net.ipv6.conf.eth0.autoconf = 0
353
+ #net.ipv6.conf.all.accept_ra = 0
354
+ #net.ipv6.conf.default.accept_ra = 0
355
+ #net.ipv6.conf.eth0.accept_ra = 0
356
+ EOS
357
+ end
358
+
359
+ recipe :kamal_single_server do
360
+ name "Kamal Single Server Scaffold"
361
+
362
+ steps do
363
+ step :set_hostname do
364
+ upload_file message: "Setting hostname",
365
+ local_file: :hostname,
366
+ remote_path: "/etc/hostname"
367
+
368
+ shell do
369
+ <<~EOS
370
+ hostname <%= Consist.config[:hostname] %>
371
+ EOS
372
+ end
373
+
374
+ mutate_file mode: :replace, target_file: "/etc/hosts", match: "^127.0.0.1 localhost$",
375
+ target_string: "127.0.0.1 localhost <%= hostname %>"
376
+ end
377
+
378
+ step :setup_timezone do
379
+ shell do
380
+ <<~EOS
381
+ rm /etc/localtime
382
+ EOS
383
+ end
384
+
385
+ upload_file message: "Setting Timezone",
386
+ local_file: :timezone,
387
+ remote_path: "/etc/timezone"
388
+
389
+ shell do
390
+ <<~EOS
391
+ chmod 0644 /etc/timezone
392
+ ln -s /usr/share/zoneinfo/<%= timezone %> /etc/localtime
393
+ chmod 0644 /etc/localtime
394
+ DEBIAN_FRONTEND=noninteractive dpkg-reconfigure -f noninteractive tzdata
395
+ EOS
396
+ end
397
+ end
398
+
399
+ step :update_apt_packages do
400
+ name "Updating APT packages"
401
+ required_user :root
402
+
403
+ upload_file message: "Uploading APT config...",
404
+ local_file: :apt_auto_upgrade,
405
+ remote_path: "/etc/apt/apt.conf.d/20auto-upgrades"
406
+
407
+ shell do
408
+ <<~EOS
409
+ apt-get update && apt-get upgrade -y
410
+ apt-get autoremove
411
+ apt-get autoclean
412
+ EOS
413
+ end
414
+ end
415
+
416
+ step :install_apt_packages do
417
+ name "Installing essential APT packages"
418
+ required_user :root
419
+
420
+ shell "Installing essential packages" do
421
+ <<~EOS
422
+ apt-get -y install build-essential curl git vim
423
+ EOS
424
+ end
425
+ end
426
+
427
+ step :setup_ntp do
428
+ name "Installing NTP daemon"
429
+ required_user :root
430
+
431
+ shell "Configuring NTP daemon", params: {raise_on_non_zero_exit: false} do
432
+ <<~EOS
433
+ apt-get -y remove systemd-timesyncd
434
+ timedatectl set-ntp no 2>1
435
+ apt-get -y install ntp
436
+ EOS
437
+ end
438
+
439
+ shell "Start NTP and Fail2Ban" do
440
+ <<~EOS
441
+ service ntp restart
442
+ EOS
443
+ end
444
+ end
445
+
446
+ step :install_fail2ban do
447
+ name "Installing fail2ban"
448
+ required_user :root
449
+
450
+ shell "Installing essential packages" do
451
+ <<~EOS
452
+ apt-get -y install fail2ban
453
+ EOS
454
+ end
455
+
456
+ upload_file message: "Uploading fail2ban confing", local_file: :fail2ban_config,
457
+ remote_path: "/etc/fail2ban/jail.local"
458
+
459
+ shell "Start Fail2Ban" do
460
+ <<~EOS
461
+ service fail2ban restart
462
+ systemctl enable fail2ban.service
463
+ EOS
464
+ end
465
+ end
466
+
467
+ step :setup_swap do
468
+ name "Configure and enable the swapfile"
469
+ required_user :root
470
+
471
+ check status: :nonexistant, file: "/swapfile" do
472
+ shell do
473
+ <<~EOS
474
+ fallocate -l <%= swap_size %> /swapfile
475
+ chmod 600 /swapfile
476
+ mkswap /swapfile
477
+ swapon /swapfile
478
+ echo "\n/swapfile swap swap defaults 0 0\n" >> /etc/fstab
479
+ sysctl vm.swappiness=<%=swap_swappiness%>
480
+ echo "\nvm.swappiness=<%=swap_swappiness%>\n" >> /etc/sysctl.conf
481
+ EOS
482
+ end
483
+ end
484
+ end
485
+
486
+ step :harden_ssh do
487
+ name "Harden the SSH config"
488
+
489
+ mutate_file mode: :replace, target_file: "/etc/ssh/sshd_config", match: "^#PasswordAuthentication yes$",
490
+ target_string: "PasswordAuthentication no"
491
+ mutate_file mode: :replace, target_file: "/etc/ssh/sshd_config", match: "^#PubkeyAuthentication yes$",
492
+ target_string: "PubkeyAuthentication yes"
493
+
494
+ shell do
495
+ <<~EOS
496
+ service ssh restart
497
+ EOS
498
+ end
499
+ end
500
+
501
+ step :harden_system do
502
+ name "Harden the SYSCTL settings"
503
+
504
+ upload_file message: "Uploading sysctl config...",
505
+ local_file: :sysctl_config,
506
+ remote_path: "/tmp/sysctl_config"
507
+
508
+ shell do
509
+ <<~EOS
510
+ cat /etc/sysctl.conf /tmp/sysctl_config > /etc/sysctl.conf
511
+ rm /tmp/sysctl_config
512
+ sysctl -p
513
+ EOS
514
+ end
515
+ end
516
+
517
+ step :setup_ufw do
518
+ name "Setup UFW"
519
+
520
+ shell do
521
+ <<~EOS
522
+ ufw logging on
523
+ ufw default deny incoming
524
+ ufw default allow outgoing
525
+ ufw allow 22
526
+ ufw allow 80
527
+ ufw allow 443
528
+ ufw --force enable
529
+ service ufw restart
530
+ EOS
531
+ end
532
+ end
533
+
534
+ step :setup_postfix do
535
+ name "Install Postfix for admin emails"
536
+
537
+ shell do
538
+ <<~EOS
539
+ echo "postfix postfix/mailname string <%= site_fqdn %>" | debconf-set-selections
540
+ EOS
541
+ end
542
+
543
+ shell do
544
+ <<~EOS
545
+ echo "postfix postfix/main_mailer_type string 'Internet Site'" | debconf-set-selections
546
+ EOS
547
+ end
548
+
549
+ shell do
550
+ <<~EOS
551
+ DEBIAN_FRONTEND=noninteractive apt-get install --assume-yes postfix
552
+ EOS
553
+ end
554
+ end
555
+
556
+ step :setup_logwatch do
557
+ name "Setup Logwatch to automate log reporting"
558
+
559
+ shell do
560
+ <<~EOS
561
+ DEBIAN_FRONTEND=noninteractive apt-get install --assume-yes logwatch
562
+ EOS
563
+ end
564
+
565
+ mutate_file mode: :replace, target_file: "/etc/cron.daily/00logwatch", match: "^/usr/sbin/logwatch --output mail$",
566
+ target_string: "/usr/sbin/logwatch --output mail --mailto <%= admin_email %> --detail high", delim: "#"
567
+
568
+ upload_file message: "Uploading Logwatch confing", local_file: :logwatch_config,
569
+ remote_path: "/etc/logwatch/conf"
570
+ end
571
+
572
+ step :setup_docker do
573
+ name "Setup Docker"
574
+
575
+ shell do
576
+ <<~EOS
577
+ # Add Docker's official GPG key:
578
+ apt-get update
579
+ apt-get install ca-certificates curl gnupg -y
580
+ install -m 0755 -d /etc/apt/keyrings
581
+ rm /etc/apt/keyrings/docker.gpg
582
+ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --batch --no-tty --dearmor -o /etc/apt/keyrings/docker.gpg
583
+ chmod a+r /etc/apt/keyrings/docker.gpg
584
+
585
+ # Add the repository to Apt sources:
586
+ echo \
587
+ "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
588
+ $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
589
+ tee /etc/apt/sources.list.d/docker.list > /dev/null
590
+ apt-get update
591
+
592
+ # Install Docker
593
+ apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin -y
594
+
595
+ # Make Docker start on boot
596
+ sudo systemctl enable docker.service
597
+ sudo systemctl enable containerd.service
598
+ EOS
599
+ end
600
+
601
+ shell "Create docker group", params: {raise_on_non_zero_exit: false} do
602
+ <<~EOS
603
+ # Create group
604
+ sudo groupadd docker
605
+ sudo usermod -aG docker $USER
606
+ EOS
607
+ end
608
+
609
+ shell "Create default private network", params: {raise_on_non_zero_exit: false} do
610
+ <<~EOS
611
+ # Create default private network
612
+ docker network create private
613
+ EOS
614
+ end
615
+ end
616
+ end
617
+ end
618
+ end
619
+
620
+ # vim: filetype=ruby
621
+ ```
622
+
623
+ Given a `Consistfile` you could then say `consist up <ip_address>` and
624
+ it would just work.
625
+
626
+ ## Is it good?
627
+
628
+ I think so. But I don't know, use your own brain or something. Don't listen to
629
+ me.
630
+
28
631
  ## Support
29
632
 
30
- If you want to report a bug, or have ideas, feedback or questions about the gem, [let me know via GitHub issues](https://github.com/johnmcdowall/consist/issues/new) and I will do my best to provide a helpful answer. Happy hacking!
633
+ If you want to report a bug, or have ideas, feedback or questions about the gem,
634
+ [let me know via GitHub issues](https://github.com/johnmcdowall/consist/issues/new)
635
+ and I will do my best to provide a helpful answer.
31
636
 
32
637
  ## License
33
638
 
34
- The gem is available as open source under the terms of the [MIT License](LICENSE.txt).
639
+ The gem is available as open source under the terms of the [LGPLv3 License](LICENSE.txt).
35
640
 
36
641
  ## Code of conduct
37
642
 
38
- Everyone interacting in this project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](CODE_OF_CONDUCT.md).
643
+ Everyone interacting in this project’s codebases, issue trackers, chat
644
+ rooms and mailing lists is expected to follow the [code of conduct](CODE_OF_CONDUCT.md).
39
645
 
40
646
  ## Contribution guide
41
647
 
42
- Pull requests are welcome!
648
+ Pull requests are welcome, but I want you to open an Issue first to discuss your
649
+ ideas. Thanks.
650
+
651
+ ## Development
652
+
653
+ 1. Clone the repo
654
+ 2. Run `bundle install`
655
+ 3. Run `bin/dev` to execute consist locally without having to build and install.
656
+
657
+ Make sure any PRs have been formatted with `standard`.