deployed 0.1.2 → 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 +4 -4
- data/README.md +29 -12
- data/app/assets/javascripts/deployed/application.js +74 -37
- data/app/assets/stylesheets/deployed/deployed.css +10 -132
- data/app/controllers/deployed/application_controller.rb +45 -0
- data/app/controllers/deployed/log_output_controller.rb +67 -0
- data/app/controllers/deployed/run_controller.rb +11 -142
- data/app/views/layouts/deployed/application.html.erb +11 -6
- data/config/routes.rb +3 -2
- data/lib/deployed/version.rb +1 -1
- data/lib/tasks/deployed_tasks.rake +36 -0
- metadata +5 -5
- data/app/views/layouts/deployed/_nav_menu.html.erb +0 -54
- data/lib/tasks/kamal_rails_tasks.rake +0 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 19518ebac39f541c9756046ccb53d48a531748beef2e080e06de2f151474d2a8
|
4
|
+
data.tar.gz: 123c0017310d3679ccff32ea35c7c951ad71a1286045308b5081202284f68d78
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f1d99d5a584bc58c659c195777a767296d831ded16af54180e6a1910c83e3f77fa260231d68a94392da7b7449fff92320f433f13a74a7e04282df23718d19bae
|
7
|
+
data.tar.gz: 1d39f6af522fc6355e5ed6b290997556c737e58d474f8b093f0c4b85d24ac1bbd61073ccf46bb934bef54fc96f22d7bbed42911cf6b5312fb79f1e0ab1b42f5f
|
data/README.md
CHANGED
@@ -1,29 +1,46 @@
|
|
1
1
|
# Deployed
|
2
2
|
|
3
|
+
[](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
|
-
|
6
|
-
|
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
|
-
|
17
|
+
group :development do
|
18
|
+
gem 'kamal'
|
19
|
+
gem 'deployed'
|
20
|
+
end
|
13
21
|
```
|
14
22
|
|
15
|
-
|
16
|
-
```bash
|
17
|
-
$ bundle
|
18
|
-
```
|
23
|
+
## Usage
|
19
24
|
|
20
|
-
|
21
|
-
|
22
|
-
|
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
|
-
|
26
|
-
|
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
|
-
|
8
|
-
|
9
|
-
|
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
|
-
|
13
|
+
window.logEventSource = new EventSource(`/deployed/log_output`)
|
20
14
|
|
21
|
-
|
15
|
+
window.logEventSource.onmessage = (event) => {
|
22
16
|
if (!Alpine.store('process').running) {
|
23
|
-
|
17
|
+
window.logEventSource.close()
|
24
18
|
} else {
|
25
|
-
if (event.data.includes(
|
26
|
-
|
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
|
50
|
-
|
51
|
-
var source = new EventSource(`/deployed/cancel`)
|
78
|
+
outputContainerEl.innerHTML += `<div class="text-red-400">Aborting...</div>`
|
52
79
|
|
53
|
-
|
54
|
-
if (event.data.includes('[Deployed Rails] End transmission')) {
|
55
|
-
source.close()
|
80
|
+
let endpoint = `/deployed/cancel`
|
56
81
|
|
57
|
-
|
58
|
-
|
59
|
-
|
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
|
-
|
16
|
-
|
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
|
-
|
30
|
-
$stdout.reopen(write_io)
|
31
|
-
|
32
|
-
# Execute the command
|
33
|
-
exec("kamal #{command}; echo \"[Deployed 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
|
-
|
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
|
-
|
99
|
-
|
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
|
-
|
110
|
-
logger.info 'Client Disconnected'
|
111
|
-
rescue IOError
|
112
|
-
logger.info 'IOError'
|
113
|
-
ensure
|
114
|
-
sse.write(
|
115
|
-
'[Deployed 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:
|
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
|
-
|
7
|
-
|
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
|
data/lib/deployed/version.rb
CHANGED
@@ -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.
|
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-
|
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/
|
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>
|