deployed 0.1.1 → 0.1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d3ebb54d143ee3d295761901280d33d36b54434c72e8a5bcf51f08d58776097a
4
- data.tar.gz: c0f7fda61b135570affd49c7f1ae3c79d7d7e119e4bb473c85a9de2b4dd50d10
3
+ metadata.gz: 19518ebac39f541c9756046ccb53d48a531748beef2e080e06de2f151474d2a8
4
+ data.tar.gz: 123c0017310d3679ccff32ea35c7c951ad71a1286045308b5081202284f68d78
5
5
  SHA512:
6
- metadata.gz: 746c98e426a43f368c80cf4824d8583c6a5821ebdf5052fc102abdeada7f114c877925f991966da667160567394133d8de2252b1a2c74abc4e92434610df5a3a
7
- data.tar.gz: cf4b4e9fde6f276b0908a2951a2ff86a7eecff60bc8f6c057d4e331d118fba871f9b409cf294d26d7798b1dc2d85cc364726b6fc2d975b53ae32c4a48ff3269c
6
+ metadata.gz: f1d99d5a584bc58c659c195777a767296d831ded16af54180e6a1910c83e3f77fa260231d68a94392da7b7449fff92320f433f13a74a7e04282df23718d19bae
7
+ data.tar.gz: 1d39f6af522fc6355e5ed6b290997556c737e58d474f8b093f0c4b85d24ac1bbd61073ccf46bb934bef54fc96f22d7bbed42911cf6b5312fb79f1e0ab1b42f5f
data/README.md CHANGED
@@ -1,29 +1,46 @@
1
1
  # Deployed
2
2
 
3
+ [![Gem Version](https://badge.fury.io/rb/deployed.svg)](https://badge.fury.io/rb/deployed)
4
+
3
5
  Deployed is a web interface for the deployment library, [Kamal](https://kamal-deploy.org).
4
6
 
5
- ## Usage
6
- How to use my plugin.
7
+ Here is a quick video demo: https://x.com/geetfun/status/1716109581619744781?s=20
8
+
9
+ ## Requirements
10
+
11
+ Ruby on Rails
7
12
 
8
13
  ## Installation
9
14
  Add this line to your application's Gemfile:
10
15
 
11
16
  ```ruby
12
- gem "deployed"
17
+ group :development do
18
+ gem 'kamal'
19
+ gem 'deployed'
20
+ end
13
21
  ```
14
22
 
15
- And then execute:
16
- ```bash
17
- $ bundle
18
- ```
23
+ ## Usage
19
24
 
20
- Or install it yourself as:
21
- ```bash
22
- $ gem install deployed
25
+ Add the following to your app's routes file:
26
+
27
+ ```ruby
28
+ Rails.application.routes.draw do
29
+ if Rails.env.development? && defined?(Deployed)
30
+ mount(Deployed::Engine => '/deployed')
31
+ end
32
+
33
+ # Your other routes...
34
+ end
23
35
  ```
24
36
 
25
- ## Contributing
26
- Contribution directions go here.
37
+ Next, head to `http://localhost:3000/deployed`
38
+
39
+ ## Development
40
+
41
+ Run `bin/setup` to bootstrap the development environment.
42
+
43
+ To run tests: `bundle exec rake app:test`. Currently there are no tests, but some will be added soon.
27
44
 
28
45
  ## License
29
46
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -4,31 +4,20 @@ import Alpine from 'https://cdn.skypack.dev/alpinejs'
4
4
  window.Alpine = Alpine
5
5
  Alpine.start()
6
6
 
7
- window.execDeployed = (commandToRun) => {
8
- // Let the frontend know we're starting
9
- Alpine.store('process').start()
10
-
11
- let outputContainerEl = document.getElementById('deploy-output')
12
- let spinnerEl = document.getElementById('spinner')
13
-
14
- if (outputContainerEl.innerHTML !== '') {
15
- outputContainerEl.innerHTML += "<div class='py-2'></div>"
16
- }
7
+ const endMarker = '[Deployed Rails] End'
8
+ const outputContainerEl = document.getElementById('deploy-output')
9
+ const spinnerEl = document.getElementById('spinner')
17
10
 
11
+ window.pipeLogs = () => {
18
12
  spinnerEl.classList.remove('hidden')
19
- var source = new EventSource(`/deployed/execute?command=${commandToRun}`)
13
+ window.logEventSource = new EventSource(`/deployed/log_output`)
20
14
 
21
- source.onmessage = (event) => {
15
+ window.logEventSource.onmessage = (event) => {
22
16
  if (!Alpine.store('process').running) {
23
- source.close()
17
+ window.logEventSource.close()
24
18
  } else {
25
- if (event.data.includes('[Deployed Rails] End transmission')) {
26
- source.close()
27
- outputContainerEl.innerHTML += `<div class="text-slate-400 pb-4">Executed: <span class='text-slate-400 font-semibold'>kamal ${commandToRun}</span></div>`
28
- spinnerEl.classList.add('hidden')
29
-
30
- // Let the frontend know we're done
31
- Alpine.store('process').stop()
19
+ if (event.data.includes("[Deployed] End")) {
20
+ window.stopPipeLogs()
32
21
  } else {
33
22
  outputContainerEl.innerHTML += event.data
34
23
  }
@@ -39,6 +28,46 @@ window.execDeployed = (commandToRun) => {
39
28
  }
40
29
  }
41
30
 
31
+ window.stopPipeLogs = () => {
32
+ if (typeof(window.logEventSource) !== 'undefined') {
33
+ window.logEventSource.close()
34
+ }
35
+ spinnerEl.classList.add('hidden')
36
+ Alpine.store('process').stop()
37
+ }
38
+
39
+ window.execDeployed = (commandToRun) => {
40
+ Alpine.store('process').start()
41
+
42
+ let endpoint = `/deployed/execute`
43
+
44
+ // Create a data object with your payload (in this case, a command)
45
+ const data = { command: commandToRun }
46
+
47
+ // Define the fetch options for the POST request
48
+ const options = {
49
+ method: 'POST',
50
+ headers: { 'Content-Type': 'application/json' },
51
+ body: JSON.stringify(data)
52
+ }
53
+
54
+ // Perform the POST request using the fetch API
55
+ fetch(endpoint, options)
56
+ .then(response => {
57
+ if (response.ok) {
58
+ outputContainerEl.innerHTML += "<div class='py-2'></div>"
59
+ outputContainerEl.innerHTML += `<div class='text-slate-400'>[Deployed] Command Received: kamal ${commandToRun}</div>`
60
+ window.pipeLogs()
61
+ return response.json(); // Parse the JSON response if needed
62
+ } else {
63
+ throw new Error('Network response was not ok');
64
+ }
65
+ })
66
+ .catch(error => {
67
+ console.error('Fetch error:', error)
68
+ })
69
+ }
70
+
42
71
  window.abortDeployed = () => {
43
72
  // Let the frontend know we're starting
44
73
  Alpine.store('process').startAbort()
@@ -46,26 +75,34 @@ window.abortDeployed = () => {
46
75
  let outputContainerEl = document.getElementById('deploy-output')
47
76
  let spinnerEl = document.getElementById('spinner')
48
77
 
49
- outputContainerEl.innerHTML += `<div class="text-red-400 py-4">Aborting...</div>`
50
-
51
- var source = new EventSource(`/deployed/cancel`)
78
+ outputContainerEl.innerHTML += `<div class="text-red-400">Aborting...</div>`
52
79
 
53
- source.onmessage = (event) => {
54
- if (event.data.includes('[Deployed Rails] End transmission')) {
55
- source.close()
80
+ let endpoint = `/deployed/cancel`
56
81
 
57
- spinnerEl.classList.add('hidden')
58
-
59
- // Let the frontend know we're done
60
- Alpine.store('process').stop()
61
- Alpine.store('process').resetAbort()
62
- } else {
63
- outputContainerEl.innerHTML += event.data
64
- }
65
-
66
- outputContainerEl.scrollIntoView({ behavior: "smooth", block: "end" })
67
- spinnerEl.scrollIntoView({ behavior: "smooth", block: "end" })
82
+ const options = {
83
+ method: 'POST',
84
+ headers: { 'Content-Type': 'application/json' }
68
85
  }
86
+
87
+ // Perform the POST request using the fetch API
88
+ fetch(endpoint, options)
89
+ .then(response => {
90
+ if (response.ok) {
91
+ window.stopPipeLogs()
92
+ Alpine.store('process').stop()
93
+ Alpine.store('process').resetAbort()
94
+ return response.json(); // Parse the JSON response if needed
95
+ } else {
96
+ throw new Error('Network response was not ok');
97
+ }
98
+ })
99
+ .then(data => {
100
+ console.log(data)
101
+ outputContainerEl.innerHTML += `<div class="text-yellow-400">Aborted process with PID ${data.message}</div>`
102
+ })
103
+ .catch(error => {
104
+ console.error('Fetch error:', error)
105
+ })
69
106
  }
70
107
 
71
108
  // Some other JS that probably should be refactored at some point...
@@ -725,10 +725,6 @@ select {
725
725
  left: 0px;
726
726
  }
727
727
 
728
- .left-\[-225px\] {
729
- left: -225px;
730
- }
731
-
732
728
  .right-0 {
733
729
  right: 0px;
734
730
  }
@@ -779,10 +775,6 @@ select {
779
775
  margin-bottom: 1.25rem;
780
776
  }
781
777
 
782
- .ml-4 {
783
- margin-left: 1rem;
784
- }
785
-
786
778
  .mr-2 {
787
779
  margin-right: 0.5rem;
788
780
  }
@@ -823,10 +815,6 @@ select {
823
815
  display: none;
824
816
  }
825
817
 
826
- .h-11 {
827
- height: 2.75rem;
828
- }
829
-
830
818
  .h-5 {
831
819
  height: 1.25rem;
832
820
  }
@@ -851,10 +839,6 @@ select {
851
839
  min-height: 100%;
852
840
  }
853
841
 
854
- .w-11 {
855
- width: 2.75rem;
856
- }
857
-
858
842
  .w-5 {
859
843
  width: 1.25rem;
860
844
  }
@@ -879,37 +863,15 @@ select {
879
863
  max-width: 32rem;
880
864
  }
881
865
 
882
- .max-w-max {
883
- max-width: -moz-max-content;
884
- max-width: max-content;
885
- }
886
-
887
- .max-w-md {
888
- max-width: 28rem;
889
- }
890
-
891
866
  .flex-1 {
892
867
  flex: 1 1 0%;
893
868
  }
894
869
 
895
- .flex-auto {
896
- flex: 1 1 auto;
897
- }
898
-
899
- .flex-none {
900
- flex: none;
901
- }
902
-
903
870
  .translate-y-0 {
904
871
  --tw-translate-y: 0px;
905
872
  transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
906
873
  }
907
874
 
908
- .translate-y-1 {
909
- --tw-translate-y: 0.25rem;
910
- transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
911
- }
912
-
913
875
  .translate-y-4 {
914
876
  --tw-translate-y: 1rem;
915
877
  transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
@@ -963,25 +925,11 @@ select {
963
925
  justify-content: center;
964
926
  }
965
927
 
966
- .gap-x-1 {
967
- -moz-column-gap: 0.25rem;
968
- column-gap: 0.25rem;
969
- }
970
-
971
- .gap-x-6 {
972
- -moz-column-gap: 1.5rem;
973
- column-gap: 1.5rem;
974
- }
975
-
976
928
  .gap-x-8 {
977
929
  -moz-column-gap: 2rem;
978
930
  column-gap: 2rem;
979
931
  }
980
932
 
981
- .gap-y-1 {
982
- row-gap: 0.25rem;
983
- }
984
-
985
933
  .gap-y-4 {
986
934
  row-gap: 1rem;
987
935
  }
@@ -992,6 +940,12 @@ select {
992
940
  margin-left: calc(0.25rem * calc(1 - var(--tw-space-x-reverse)));
993
941
  }
994
942
 
943
+ .space-x-2 > :not([hidden]) ~ :not([hidden]) {
944
+ --tw-space-x-reverse: 0;
945
+ margin-right: calc(0.5rem * var(--tw-space-x-reverse));
946
+ margin-left: calc(0.5rem * calc(1 - var(--tw-space-x-reverse)));
947
+ }
948
+
995
949
  .space-y-10 > :not([hidden]) ~ :not([hidden]) {
996
950
  --tw-space-y-reverse: 0;
997
951
  margin-top: calc(2.5rem * calc(1 - var(--tw-space-y-reverse)));
@@ -1004,18 +958,6 @@ select {
1004
958
  margin-bottom: calc(1rem * var(--tw-space-y-reverse));
1005
959
  }
1006
960
 
1007
- .space-x-3 > :not([hidden]) ~ :not([hidden]) {
1008
- --tw-space-x-reverse: 0;
1009
- margin-right: calc(0.75rem * var(--tw-space-x-reverse));
1010
- margin-left: calc(0.75rem * calc(1 - var(--tw-space-x-reverse)));
1011
- }
1012
-
1013
- .space-x-2 > :not([hidden]) ~ :not([hidden]) {
1014
- --tw-space-x-reverse: 0;
1015
- margin-right: calc(0.5rem * var(--tw-space-x-reverse));
1016
- margin-left: calc(0.5rem * calc(1 - var(--tw-space-x-reverse)));
1017
- }
1018
-
1019
961
  .divide-y > :not([hidden]) ~ :not([hidden]) {
1020
962
  --tw-divide-y-reverse: 0;
1021
963
  border-top-width: calc(1px * calc(1 - var(--tw-divide-y-reverse)));
@@ -1043,10 +985,6 @@ select {
1043
985
  overflow-y: auto;
1044
986
  }
1045
987
 
1046
- .rounded-3xl {
1047
- border-radius: 1.5rem;
1048
- }
1049
-
1050
988
  .rounded-full {
1051
989
  border-radius: 9999px;
1052
990
  }
@@ -1067,18 +1005,14 @@ select {
1067
1005
  border-width: 1px;
1068
1006
  }
1069
1007
 
1070
- .border-t {
1071
- border-top-width: 1px;
1072
- }
1073
-
1074
- .border-b-2 {
1075
- border-bottom-width: 2px;
1076
- }
1077
-
1078
1008
  .border-b-4 {
1079
1009
  border-bottom-width: 4px;
1080
1010
  }
1081
1011
 
1012
+ .border-t {
1013
+ border-top-width: 1px;
1014
+ }
1015
+
1082
1016
  .border-slate-200 {
1083
1017
  --tw-border-opacity: 1;
1084
1018
  border-color: rgb(226 232 240 / var(--tw-border-opacity));
@@ -1089,11 +1023,6 @@ select {
1089
1023
  border-color: rgb(203 213 225 / var(--tw-border-opacity));
1090
1024
  }
1091
1025
 
1092
- .bg-gray-50 {
1093
- --tw-bg-opacity: 1;
1094
- background-color: rgb(249 250 251 / var(--tw-bg-opacity));
1095
- }
1096
-
1097
1026
  .bg-gray-500 {
1098
1027
  --tw-bg-opacity: 1;
1099
1028
  background-color: rgb(107 114 128 / var(--tw-bg-opacity));
@@ -1261,14 +1190,6 @@ select {
1261
1190
  padding-top: 2.5rem;
1262
1191
  }
1263
1192
 
1264
- .pt-\[52px\] {
1265
- padding-top: 52px;
1266
- }
1267
-
1268
- .pt-\[55px\] {
1269
- padding-top: 55px;
1270
- }
1271
-
1272
1193
  .pt-\[56px\] {
1273
1194
  padding-top: 56px;
1274
1195
  }
@@ -1334,11 +1255,6 @@ select {
1334
1255
  color: rgb(107 114 128 / var(--tw-text-opacity));
1335
1256
  }
1336
1257
 
1337
- .text-gray-600 {
1338
- --tw-text-opacity: 1;
1339
- color: rgb(75 85 99 / var(--tw-text-opacity));
1340
- }
1341
-
1342
1258
  .text-gray-700 {
1343
1259
  --tw-text-opacity: 1;
1344
1260
  color: rgb(55 65 81 / var(--tw-text-opacity));
@@ -1424,12 +1340,6 @@ select {
1424
1340
  opacity: 0.75;
1425
1341
  }
1426
1342
 
1427
- .shadow-lg {
1428
- --tw-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
1429
- --tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);
1430
- box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
1431
- }
1432
-
1433
1343
  .shadow-md {
1434
1344
  --tw-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
1435
1345
  --tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color);
@@ -1467,14 +1377,6 @@ select {
1467
1377
  --tw-ring-color: rgb(17 24 39 / 0.05);
1468
1378
  }
1469
1379
 
1470
- .transition {
1471
- transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, -webkit-backdrop-filter;
1472
- transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter;
1473
- transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter, -webkit-backdrop-filter;
1474
- transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
1475
- transition-duration: 150ms;
1476
- }
1477
-
1478
1380
  .transition-all {
1479
1381
  transition-property: all;
1480
1382
  transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
@@ -1487,10 +1389,6 @@ select {
1487
1389
  transition-duration: 150ms;
1488
1390
  }
1489
1391
 
1490
- .duration-150 {
1491
- transition-duration: 150ms;
1492
- }
1493
-
1494
1392
  .duration-200 {
1495
1393
  transition-duration: 200ms;
1496
1394
  }
@@ -1569,16 +1467,6 @@ select {
1569
1467
  outline-color: #0284c7;
1570
1468
  }
1571
1469
 
1572
- .group:hover .group-hover\:bg-white {
1573
- --tw-bg-opacity: 1;
1574
- background-color: rgb(255 255 255 / var(--tw-bg-opacity));
1575
- }
1576
-
1577
- .group:hover .group-hover\:text-indigo-600 {
1578
- --tw-text-opacity: 1;
1579
- color: rgb(79 70 229 / var(--tw-text-opacity));
1580
- }
1581
-
1582
1470
  @media (min-width: 640px) {
1583
1471
  .sm\:col-span-2 {
1584
1472
  grid-column: span 2 / span 2;
@@ -1623,13 +1511,3 @@ select {
1623
1511
  grid-template-columns: repeat(3, minmax(0, 1fr));
1624
1512
  }
1625
1513
  }
1626
-
1627
- @media (min-width: 1024px) {
1628
- .lg\:max-w-2xl {
1629
- max-width: 42rem;
1630
- }
1631
-
1632
- .lg\:grid-cols-2 {
1633
- grid-template-columns: repeat(2, minmax(0, 1fr));
1634
- }
1635
- }
@@ -14,5 +14,50 @@ module Deployed
14
14
  def initialize_deployed
15
15
  Deployed.setup!
16
16
  end
17
+
18
+ def lock_file_path
19
+ Rails.root.join(Deployed::DIRECTORY, 'process.lock')
20
+ end
21
+
22
+ def lock_process
23
+ File.open(lock_file_path, 'a') do |file|
24
+ file.puts(Deployed::CurrentExecution.child_pid)
25
+ end
26
+ end
27
+
28
+ def release_process
29
+ return unless File.exist?(lock_file_path)
30
+
31
+ File.delete(lock_file_path)
32
+ end
33
+
34
+ def stored_pid
35
+ return false unless File.exist?(lock_file_path)
36
+
37
+ value = File.read(lock_file_path).to_i
38
+
39
+ if value.is_a?(Integer)
40
+ value
41
+ else
42
+ false
43
+ end
44
+ end
45
+
46
+ def process_running?
47
+ return false unless stored_pid
48
+
49
+ begin
50
+ # Send signal 0 to the process to check if it exists
51
+ Process.kill(0, stored_pid)
52
+ true
53
+ rescue Errno::ESRCH
54
+ false
55
+ end
56
+ end
57
+ helper_method :process_running?
58
+
59
+ def current_log_file
60
+ Rails.root.join(Deployed::DIRECTORY, 'deployments/current.log')
61
+ end
17
62
  end
18
63
  end
@@ -0,0 +1,67 @@
1
+ module Deployed
2
+ class LogOutputController < ApplicationController
3
+ include ActionController::Live
4
+
5
+ before_action :set_headers
6
+
7
+ def index
8
+ thread_exit_flag = false
9
+
10
+ thread = Thread.new do
11
+ File.open(current_log_file, 'r') do |file|
12
+ while true
13
+ IO.select([file])
14
+
15
+ found_deployed = false
16
+
17
+ file.each_line do |line|
18
+ # Check the exit flag
19
+ if thread_exit_flag
20
+ break
21
+ end
22
+
23
+ css_class = if line.include?('[Deployed]')
24
+ 'text-slate-400'
25
+ else
26
+ 'text-green-400'
27
+ end
28
+ sse.write("<div class='#{css_class}'>#{line.strip}</div>", event: 'message')
29
+
30
+ if line.include?("[Deployed Rails] End")
31
+ found_deployed = true
32
+ break
33
+ end
34
+ end
35
+
36
+ if found_deployed || thread_exit_flag
37
+ break
38
+ end
39
+ end
40
+ end
41
+ end
42
+
43
+ begin
44
+ thread.join
45
+ rescue ActionController::Live::ClientDisconnected
46
+ logger.info 'Client Disconnected'
47
+ ensure
48
+ # Set the exit flag to true to signal the thread to exit
49
+ thread_exit_flag = true
50
+ sse.close
51
+ response.stream.close
52
+ end
53
+ end
54
+
55
+
56
+ private
57
+
58
+ def set_headers
59
+ response.headers['Content-Type'] = 'text/event-stream'
60
+ response.headers['Last-Modified'] = Time.now.httpdate
61
+ end
62
+
63
+ def sse
64
+ @sse ||= SSE.new(response.stream, event: 'Stream Started')
65
+ end
66
+ end
67
+ end
@@ -4,175 +4,44 @@ module Deployed
4
4
  # Provides a centralized way to run all `kamal [command]` executions and streams to the browser
5
5
  class RunController < ApplicationController
6
6
  class ConcurrentProcessRunning < StandardError; end
7
-
8
- include ActionController::Live
9
-
10
- before_action :set_headers
7
+ skip_forgery_protection
11
8
 
12
9
  # Endpoint to execute the kamal command
13
10
  def execute
14
- if process_running?
15
- raise(ConcurrentProcessRunning)
16
- elsif stored_pid
17
- release_process
18
- end
19
-
20
- sse.write(
21
- "<div class='text-slate-400'>> <span class='text-slate-300 font-semibold'>kamal #{command}</span></div>",
22
- event: 'message'
23
- )
24
-
25
- read_io, write_io = IO.pipe
11
+ raise(ConcurrentProcessRunning) if process_running?
12
+ release_process if stored_pid
13
+ File.write(current_log_file, '')
26
14
 
27
15
  # Fork a child process
28
16
  Deployed::CurrentExecution.child_pid = fork do
29
- # Redirect the standard output to the write end of the pipe
30
- $stdout.reopen(write_io)
31
-
32
- # Execute the command
33
- exec("kamal #{command}; echo \"[Kamal Rails] End transmission\"")
17
+ exec("bundle exec rake deployed:execute_and_log['#{command}']")
34
18
  end
35
19
 
36
20
  lock_process
37
-
38
- sse.write(
39
- "<div class='text-slate-400' data-child-pid=\"#{Deployed::CurrentExecution.child_pid}\"></div>",
40
- event: 'message'
41
- )
42
-
43
- write_io.close
44
-
45
- # Use a separate thread to read and stream the output
46
- output_thread = Thread.new do
47
- read_io.each_line do |line|
48
- output = line.strip
49
- output = output.gsub('49.13.91.176', '[redacted]')
50
- text_color_class = 'text-green-400'
51
-
52
- # Hackish way of dealing with error messages at the moment
53
- if output.include?('[31m')
54
- text_color_class = 'text-red-500'
55
- output.gsub!('[31m', '')
56
- output.gsub!('[0m', '')
57
- end
58
-
59
- sse.write("<div class='#{text_color_class}'>#{output}</div>", event: 'message')
60
- end
61
-
62
- # Ensure the response stream and the thread are closed properly
63
- sse.close
64
- response.stream.close
65
- end
66
-
67
- # Ensure that the thread is joined when the execution is complete
68
- Process.wait
69
- output_thread.join
21
+ render json: { message: 'OK' }
70
22
  rescue ConcurrentProcessRunning
71
- sse.write(
72
- "<div class='text-red-500'>Existing process running with PID: #{stored_pid}</div>",
73
- event: 'message'
74
- )
75
- logger.info 'Existing process running'
76
- rescue ActionController::Live::ClientDisconnected
77
- logger.info 'Client Disconnected'
78
- rescue IOError
79
- logger.info 'IOError'
80
- ensure
81
- sse.close
82
- response.stream.close
83
- release_process
23
+ render json: { message: 'EXISTING PROCESS' }
84
24
  end
85
25
 
86
26
  # Endpoint to cancel currently running process
87
27
  def cancel
28
+ pid = stored_pid
88
29
  if process_running?
89
30
  # If a process is running, get the PID and attempt to kill it
90
31
  begin
91
32
  Process.kill('TERM', stored_pid)
92
- sse.write(
93
- "<div class='text-yellow-400'>Cancelled the process with PID: #{stored_pid}</div>",
94
- event: 'message'
95
- )
96
- release_process
97
33
  rescue Errno::ESRCH
98
- sse.write(
99
- "<div class='text-red-500'>Process with PID #{stored_pid} is not running.</div>",
100
- event: 'message'
101
- )
34
+ ensure
35
+ release_process
102
36
  end
103
- else
104
- sse.write(
105
- "<div class='text-slate-400'>No process is currently running, nothing to cancel.</div>",
106
- event: 'message'
107
- )
108
37
  end
109
- rescue ActionController::Live::ClientDisconnected
110
- logger.info 'Client Disconnected'
111
- rescue IOError
112
- logger.info 'IOError'
113
- ensure
114
- sse.write(
115
- '[Kamal Rails] End transmission',
116
- event: 'message'
117
- )
118
- sse.close
119
- response.stream.close
120
- release_process
38
+ render json: { message: pid }
121
39
  end
122
40
 
123
41
  private
124
42
 
125
- def set_headers
126
- response.headers['Content-Type'] = 'text/event-stream'
127
- response.headers['Last-Modified'] = Time.now.httpdate
128
- end
129
-
130
- def sse
131
- @sse ||= SSE.new(response.stream, event: 'Stream Started')
132
- end
133
-
134
43
  def command
135
44
  params[:command]
136
45
  end
137
-
138
- def lock_file_path
139
- Rails.root.join(Deployed::DIRECTORY, 'process.lock')
140
- end
141
-
142
- def lock_process
143
- File.open(lock_file_path, 'a') do |file|
144
- file.puts(Deployed::CurrentExecution.child_pid)
145
- end
146
- end
147
-
148
- def release_process
149
- return unless File.exist?(lock_file_path)
150
-
151
- File.delete(lock_file_path)
152
- end
153
-
154
- def stored_pid
155
- return false unless File.exist?(lock_file_path)
156
-
157
- value = File.read(lock_file_path).to_i
158
-
159
- if value.is_a?(Integer)
160
- value
161
- else
162
- false
163
- end
164
- end
165
-
166
- def process_running?
167
- return false unless stored_pid
168
-
169
- begin
170
- # Send signal 0 to the process to check if it exists
171
- Process.kill(0, stored_pid)
172
- true
173
- rescue Errno::ESRCH
174
- false
175
- end
176
- end
177
46
  end
178
47
  end
@@ -10,7 +10,7 @@
10
10
  <script>
11
11
  document.addEventListener('alpine:init', () => {
12
12
  Alpine.store('process', {
13
- running: false,
13
+ running: <%= process_running? %>,
14
14
  start() {
15
15
  this.running = true
16
16
  },
@@ -47,14 +47,12 @@
47
47
  DEPLOYED
48
48
  </div>
49
49
  </div>
50
+ <div>
51
+ <span class="text-base text-slate-400">v<%= Deployed::VERSION %></span>
52
+ </div>
50
53
  </div>
51
54
  </h1>
52
55
  </div>
53
- <% if false %>
54
- <div class="ml-4">
55
- <%= render 'layouts/deployed/nav_menu' %>
56
- </div>
57
- <% end %>
58
56
  </div>
59
57
  </header>
60
58
  <div class="h-screen flex flex-col pt-[56px]">
@@ -86,5 +84,12 @@
86
84
  </div>
87
85
 
88
86
  <%= turbo_frame_tag('deployed-init', src: setup_path, target: '_top') if Deployed::Config.requires_init %>
87
+
88
+ <!-- If we refresh the page, we want to see the logs still piping... -->
89
+ <script>
90
+ setTimeout(() => {
91
+ <% if process_running? %>window.pipeLogs()<% end %>
92
+ }, 1000)
93
+ </script>
89
94
  </body>
90
95
  </html>
data/config/routes.rb CHANGED
@@ -3,7 +3,8 @@ Deployed::Engine.routes.draw do
3
3
  post 'setup', to: 'setup#create'
4
4
  get 'config', to: 'config#show'
5
5
  get 'git/uncommitted_check', to: 'git#uncommitted_check'
6
- get 'execute', to: 'run#execute'
7
- get 'cancel', to: 'run#cancel'
6
+ post 'execute', to: 'run#execute'
7
+ post 'cancel', to: 'run#cancel'
8
+ get 'log_output', to: 'log_output#index'
8
9
  root to: 'welcome#index'
9
10
  end
@@ -1,3 +1,3 @@
1
1
  module Deployed
2
- VERSION = "0.1.1"
2
+ VERSION = "0.1.3"
3
3
  end
@@ -0,0 +1,36 @@
1
+ # lib/tasks/deployed.rake
2
+
3
+ namespace :deployed do
4
+ desc "Execute a Kamal command and log its output"
5
+ task :execute_and_log, [:command] => :environment do |task, args|
6
+ command = args[:command]
7
+
8
+ unless command
9
+ puts "Please provide a Kamal command. Usage: rake deployed:execute_and_log[command]"
10
+ next
11
+ end
12
+
13
+ log_file = Rails.root.join(Deployed::DIRECTORY, 'deployments/current.log')
14
+
15
+ File.open(log_file, 'a') do |file|
16
+ IO.popen("kamal #{command}") do |io|
17
+ start_time = Time.now
18
+
19
+ file.puts("[Deployed] > kamal #{command}")
20
+ file.fsync
21
+
22
+ io.each_line do |line|
23
+ file.puts line
24
+ file.fsync # Force data to be written to disk immediately
25
+ end
26
+ end_time = Time.now
27
+ file.puts("[Deployed] Finished in #{end_time - start_time} seconds")
28
+ file.puts("[Deployed] End")
29
+ file.fsync
30
+
31
+ # Delete lockfile
32
+ File.delete(Rails.root.join(Deployed::DIRECTORY, 'process.lock'))
33
+ end
34
+ end
35
+ end
36
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: deployed
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.1.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Simon Chiu
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-10-23 00:00:00.000000000 Z
11
+ date: 2023-10-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: kamal
@@ -69,6 +69,7 @@ files:
69
69
  - app/controllers/deployed/application_controller.rb
70
70
  - app/controllers/deployed/config_controller.rb
71
71
  - app/controllers/deployed/git_controller.rb
72
+ - app/controllers/deployed/log_output_controller.rb
72
73
  - app/controllers/deployed/run_controller.rb
73
74
  - app/controllers/deployed/setup_controller.rb
74
75
  - app/controllers/deployed/welcome_controller.rb
@@ -79,13 +80,12 @@ files:
79
80
  - app/views/deployed/git/uncommitted_check.html.erb
80
81
  - app/views/deployed/setup/new.html.erb
81
82
  - app/views/deployed/welcome/index.html.erb
82
- - app/views/layouts/deployed/_nav_menu.html.erb
83
83
  - app/views/layouts/deployed/application.html.erb
84
84
  - config/routes.rb
85
85
  - lib/deployed.rb
86
86
  - lib/deployed/engine.rb
87
87
  - lib/deployed/version.rb
88
- - lib/tasks/kamal_rails_tasks.rake
88
+ - lib/tasks/deployed_tasks.rake
89
89
  homepage: https://github.com/geetfun/deployed
90
90
  licenses:
91
91
  - MIT
@@ -93,7 +93,7 @@ metadata:
93
93
  allowed_push_host: https://rubygems.org
94
94
  homepage_uri: https://github.com/geetfun/deployed
95
95
  source_code_uri: https://github.com/geetfun/deployed
96
- changelog_uri: https://github.com/geetfun/deployed
96
+ changelog_uri: https://github.com/geetfun/deployed/blob/main/CHANGELOG.md
97
97
  post_install_message:
98
98
  rdoc_options: []
99
99
  require_paths:
@@ -1,54 +0,0 @@
1
- <div class="relative">
2
- <button type="button" class="inline-flex items-center gap-x-1 text-sm font-semibold leading-6 text-slate-400" aria-expanded="false">
3
- <span>Resources</span>
4
- <svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
5
- <path fill-rule="evenodd" d="M5.23 7.21a.75.75 0 011.06.02L10 11.168l3.71-3.938a.75.75 0 111.08 1.04l-4.25 4.5a.75.75 0 01-1.08 0l-4.25-4.5a.75.75 0 01.02-1.06z" clip-rule="evenodd" />
6
- </svg>
7
- </button>
8
-
9
- <!--
10
- Flyout menu, show/hide based on flyout menu state.
11
-
12
- Entering: "transition ease-out duration-200"
13
- From: "opacity-0 translate-y-1"
14
- To: "opacity-100 translate-y-0"
15
- Leaving: "transition ease-in duration-150"
16
- From: "opacity-100 translate-y-0"
17
- To: "opacity-0 translate-y-1"
18
- -->
19
- <div class="absolute z-10 mt-5 flex w-screen max-w-max left-[-225px] px-4">
20
- <div class="w-screen max-w-md flex-auto overflow-hidden rounded-3xl bg-white text-sm leading-6 shadow-lg ring-1 ring-gray-900/5 lg:max-w-2xl">
21
- <div class="grid grid-cols-1 gap-x-6 gap-y-1 p-4 lg:grid-cols-2">
22
- <div class="group relative flex gap-x-6 rounded-lg p-4 hover:bg-gray-50">
23
- <div class="mt-1 flex h-11 w-11 flex-none items-center justify-center rounded-lg bg-gray-50 group-hover:bg-white">
24
- <svg class="h-6 w-6 text-gray-600 group-hover:text-indigo-600" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true">
25
- <path stroke-linecap="round" stroke-linejoin="round" d="M10.5 6a7.5 7.5 0 107.5 7.5h-7.5V6z" />
26
- <path stroke-linecap="round" stroke-linejoin="round" d="M13.5 10.5H21A7.5 7.5 0 0013.5 3v7.5z" />
27
- </svg>
28
- </div>
29
- <div>
30
- <a href="#" class="font-semibold text-gray-900">
31
- Documentation
32
- <span class="absolute inset-0"></span>
33
- </a>
34
- <p class="mt-1 text-gray-600">Check out our documentation</p>
35
- </div>
36
- </div>
37
- <div class="group relative flex gap-x-6 rounded-lg p-4 hover:bg-gray-50">
38
- <div class="mt-1 flex h-11 w-11 flex-none items-center justify-center rounded-lg bg-gray-50 group-hover:bg-white">
39
- <svg class="h-6 w-6 text-gray-600 group-hover:text-indigo-600" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true">
40
- <path stroke-linecap="round" stroke-linejoin="round" d="M13.5 16.875h3.375m0 0h3.375m-3.375 0V13.5m0 3.375v3.375M6 10.5h2.25a2.25 2.25 0 002.25-2.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v2.25A2.25 2.25 0 006 10.5zm0 9.75h2.25A2.25 2.25 0 0010.5 18v-2.25a2.25 2.25 0 00-2.25-2.25H6a2.25 2.25 0 00-2.25 2.25V18A2.25 2.25 0 006 20.25zm9.75-9.75H18a2.25 2.25 0 002.25-2.25V6A2.25 2.25 0 0018 3.75h-2.25A2.25 2.25 0 0013.5 6v2.25a2.25 2.25 0 002.25 2.25z" />
41
- </svg>
42
- </div>
43
- <div>
44
- <a href="#" class="font-semibold text-gray-900">
45
- Our GitHub Repository
46
- <span class="absolute inset-0"></span>
47
- </a>
48
- <p class="mt-1 text-gray-600">Ipsum Lorem</p>
49
- </div>
50
- </div>
51
- </div>
52
- </div>
53
- </div>
54
- </div>
@@ -1,4 +0,0 @@
1
- # desc "Explaining what the task does"
2
- # task :deployed do
3
- # # Task goes here
4
- # end