shipit-engine 0.35.0 → 0.36.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +20 -7
- data/app/controllers/concerns/shipit/authentication.rb +5 -1
- data/app/controllers/shipit/api/base_controller.rb +13 -1
- data/app/controllers/shipit/api/rollbacks_controller.rb +1 -1
- data/app/controllers/shipit/api/stacks_controller.rb +8 -2
- data/app/controllers/shipit/api/tasks_controller.rb +19 -2
- data/app/controllers/shipit/rollbacks_controller.rb +5 -1
- data/app/helpers/shipit/stacks_helper.rb +11 -0
- data/app/models/concerns/shipit/deferred_touch.rb +3 -3
- data/app/models/shipit/anonymous_user.rb +4 -0
- data/app/models/shipit/api_client.rb +1 -1
- data/app/models/shipit/commit_checks.rb +3 -3
- data/app/models/shipit/delivery.rb +1 -1
- data/app/models/shipit/deploy.rb +1 -0
- data/app/models/shipit/deploy_spec/file_system.rb +32 -4
- data/app/models/shipit/pull_request.rb +1 -1
- data/app/models/shipit/stack.rb +10 -10
- data/app/models/shipit/task.rb +31 -4
- data/app/models/shipit/user.rb +23 -9
- data/app/serializers/shipit/stack_serializer.rb +1 -1
- data/app/views/shipit/deploys/_deploy.html.erb +1 -5
- data/app/views/shipit/stacks/_banners.html.erb +1 -1
- data/app/views/shipit/stacks/_settings_form.erb +55 -0
- data/app/views/shipit/stacks/settings.html.erb +1 -55
- data/app/views/shipit/stacks/show.html.erb +1 -1
- data/config/locales/en.yml +1 -1
- data/config/routes.rb +4 -0
- data/db/migrate/20211103154121_increase_github_team_slug_size.rb +5 -0
- data/lib/shipit/engine.rb +15 -5
- data/lib/shipit/stack_commands.rb +2 -2
- data/lib/shipit/task_commands.rb +1 -1
- data/lib/shipit/version.rb +1 -1
- data/lib/shipit.rb +55 -3
- data/lib/snippets/fetch-gem-version +1 -1
- data/test/controllers/api/hooks_controller_test.rb +1 -1
- data/test/controllers/api/rollback_controller_test.rb +1 -0
- data/test/controllers/api/stacks_controller_test.rb +25 -0
- data/test/controllers/api/tasks_controller_test.rb +56 -0
- data/test/controllers/stacks_controller_test.rb +11 -0
- data/test/dummy/config/application.rb +1 -2
- data/test/dummy/db/schema.rb +2 -2
- data/test/fixtures/shipit/check_runs.yml +3 -3
- data/test/fixtures/shipit/commits.yml +101 -101
- data/test/fixtures/shipit/deliveries.yml +1 -1
- data/test/fixtures/shipit/merge_requests.yml +19 -19
- data/test/fixtures/shipit/stacks.yml +28 -28
- data/test/fixtures/shipit/statuses.yml +16 -16
- data/test/fixtures/shipit/tasks.yml +77 -65
- data/test/fixtures/shipit/users.yml +2 -5
- data/test/models/commits_test.rb +6 -6
- data/test/models/deploy_spec_test.rb +0 -23
- data/test/models/deploys_test.rb +26 -0
- data/test/models/shipit/deploy_spec/file_system_test.rb +81 -0
- data/test/models/tasks_test.rb +14 -2
- data/test/models/team_test.rb +21 -2
- data/test/models/users_test.rb +29 -9
- data/test/unit/deploy_commands_test.rb +2 -2
- metadata +189 -171
@@ -6,61 +6,7 @@
|
|
6
6
|
<h2>Settings (Stack #<%= @stack.id %>)</h2>
|
7
7
|
</header>
|
8
8
|
|
9
|
-
|
10
|
-
<%= form_with scope: :stack, url: stack_path(@stack), method: :patch do |f| %>
|
11
|
-
<div class="field-wrapper">
|
12
|
-
<%= f.label :environment %>
|
13
|
-
<%= f.text_field :environment, placeholder: 'production' %>
|
14
|
-
</div>
|
15
|
-
|
16
|
-
<div class="field-wrapper">
|
17
|
-
<span>Branch: <%= @stack.branch %></span>
|
18
|
-
</div>
|
19
|
-
|
20
|
-
<div class="field-wrapper">
|
21
|
-
<%= f.label :deploy_url, 'Deploy URL (Where is this stack deployed to?)' %>
|
22
|
-
<%= f.text_field :deploy_url, placeholder: 'https://' %>
|
23
|
-
</div>
|
24
|
-
|
25
|
-
<div class="field-wrapper">
|
26
|
-
<%= f.check_box :continuous_deployment %>
|
27
|
-
<%= f.label :continuous_deployment, 'Enable continuous deployment' %>
|
28
|
-
</div>
|
29
|
-
|
30
|
-
<div class="field-wrapper">
|
31
|
-
<%= f.check_box :merge_queue_enabled %>
|
32
|
-
<%= f.label :merge_queue_enabled, 'Enable merge queue' %>
|
33
|
-
</div>
|
34
|
-
|
35
|
-
<div class="field-wrapper">
|
36
|
-
<%= f.check_box :ignore_ci %>
|
37
|
-
<%= f.label :ignore_ci, "Don't require CI to deploy" %>
|
38
|
-
</div>
|
39
|
-
|
40
|
-
<%= f.submit class: "btn", value: "Save" %>
|
41
|
-
<% end %>
|
42
|
-
</div>
|
43
|
-
|
44
|
-
<div class="setting-section">
|
45
|
-
<h5>Lock deploys</h5>
|
46
|
-
<%= form_with scope: :stack, url: stack_path(@stack), method: :patch do |f| %>
|
47
|
-
<div class="field-wrapper">
|
48
|
-
<%= f.label :lock_reason, 'Reason for lock' %>
|
49
|
-
<%= f.text_area :lock_reason %>
|
50
|
-
</div>
|
51
|
-
<% if @stack.locked? %>
|
52
|
-
<%= f.submit class: "btn", value: "Update Reason" %>
|
53
|
-
<% else %>
|
54
|
-
<%= f.submit class: "btn", value: "Lock" %>
|
55
|
-
<% end %>
|
56
|
-
<% end %>
|
57
|
-
<% if @stack.locked? %>
|
58
|
-
<%= form_with scope: :stack, url: stack_path(@stack), method: :patch do |f| %>
|
59
|
-
<%= f.hidden_field :lock_reason, value: nil %>
|
60
|
-
<%= f.submit class: "btn btn--primary", value: "Unlock" %>
|
61
|
-
<%- end -%>
|
62
|
-
<% end %>
|
63
|
-
</div>
|
9
|
+
<%= render partial: 'shipit/stacks/settings_form', locals: { stack: @stack } %>
|
64
10
|
|
65
11
|
<div class="setting-section">
|
66
12
|
<h5>Resynchronize this stack</h5>
|
data/config/locales/en.yml
CHANGED
@@ -41,7 +41,7 @@ en:
|
|
41
41
|
reject: Mark the release as faulty
|
42
42
|
deploy_button:
|
43
43
|
hint:
|
44
|
-
max_commits:
|
44
|
+
max_commits: Use caution when deploying more than %{maximum} commits at once.
|
45
45
|
blocked: This commit range includes a commit that can't be deployed.
|
46
46
|
caption:
|
47
47
|
pending: Pending CI
|
data/config/routes.rb
CHANGED
@@ -20,6 +20,7 @@ Shipit::Engine.routes.draw do
|
|
20
20
|
get '/' => 'stacks#show'
|
21
21
|
delete '/' => 'stacks#destroy'
|
22
22
|
patch '/' => 'stacks#update'
|
23
|
+
post '/refresh' => 'stacks#refresh'
|
23
24
|
end
|
24
25
|
|
25
26
|
scope '/stacks/*stack_id', stack_id: stack_id_format, as: :stack do
|
@@ -27,6 +28,9 @@ Shipit::Engine.routes.draw do
|
|
27
28
|
resource :lock, only: %i(create update destroy)
|
28
29
|
resources :tasks, only: %i(index show) do
|
29
30
|
resource :output, only: :show
|
31
|
+
member do
|
32
|
+
put :abort
|
33
|
+
end
|
30
34
|
end
|
31
35
|
resources :deploys, only: %i(index create) do
|
32
36
|
resources :release_statuses, only: %i(create)
|
data/lib/shipit/engine.rb
CHANGED
@@ -5,6 +5,17 @@ module Shipit
|
|
5
5
|
|
6
6
|
paths['app/models'] << 'app/serializers' << 'app/serializers/concerns'
|
7
7
|
|
8
|
+
initializer 'shipit.encryption_config', before: 'active_record_encryption.configuration' do |app|
|
9
|
+
if app.credentials.active_record_encryption.blank? && Shipit.user_access_tokens_key.present?
|
10
|
+
# For ease of upgrade, we derive an Active Record encryption config automatically.
|
11
|
+
# But if AR Encryption is already configured, we just use that
|
12
|
+
app.credentials[:active_record_encryption] = {
|
13
|
+
primary_key: Shipit.user_access_tokens_key,
|
14
|
+
key_derivation_salt: Digest::SHA256.digest("salt:".b + Shipit.user_access_tokens_key),
|
15
|
+
}
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
8
19
|
initializer 'shipit.config' do |app|
|
9
20
|
Rails.application.routes.default_url_options[:host] = Shipit.host
|
10
21
|
Shipit::Engine.routes.default_url_options[:host] = Shipit.host
|
@@ -28,8 +39,6 @@ module Shipit
|
|
28
39
|
path.end_with?('.svg') || (path.start_with?('emoji/') && path.end_with?('.png'))
|
29
40
|
end
|
30
41
|
|
31
|
-
ActionDispatch::ExceptionWrapper.rescue_responses[Shipit::TaskDefinition::NotFound.name] = :not_found
|
32
|
-
|
33
42
|
ActiveModel::Serializer._root = false
|
34
43
|
ActiveModel::ArraySerializer._root = false
|
35
44
|
ActiveModel::Serializer.include(Engine.routes.url_helpers)
|
@@ -44,10 +53,11 @@ module Shipit
|
|
44
53
|
if Shipit.enable_samesite_middleware?
|
45
54
|
app.config.middleware.insert_after(::Rack::Runtime, Shipit::SameSiteCookieMiddleware)
|
46
55
|
end
|
56
|
+
end
|
47
57
|
|
48
|
-
|
49
|
-
|
50
|
-
|
58
|
+
config.after_initialize do
|
59
|
+
ActionDispatch::ExceptionWrapper.rescue_responses[Shipit::TaskDefinition::NotFound.name] = :not_found
|
60
|
+
ActionController::Base.include(Shipit::ActiveModelSerializersPatch)
|
51
61
|
end
|
52
62
|
end
|
53
63
|
end
|
@@ -16,7 +16,7 @@ module Shipit
|
|
16
16
|
def fetch
|
17
17
|
create_directories
|
18
18
|
if valid_git_repository?(@stack.git_path)
|
19
|
-
git('fetch', 'origin', '--tags', @stack.branch, env: env, chdir: @stack.git_path)
|
19
|
+
git('fetch', 'origin', '--quiet', '--tags', @stack.branch, env: env, chdir: @stack.git_path)
|
20
20
|
else
|
21
21
|
@stack.clear_git_cache!
|
22
22
|
git_clone(@stack.repo_git_url, @stack.git_path, branch: @stack.branch, env: env, chdir: @stack.deploys_path)
|
@@ -72,7 +72,7 @@ module Shipit
|
|
72
72
|
).run!
|
73
73
|
|
74
74
|
git_dir = File.join(dir, @stack.repo_name)
|
75
|
-
git('
|
75
|
+
git('-c', 'advice.detachedHead=false', 'checkout', commit.sha, chdir: git_dir).run! if commit
|
76
76
|
yield Pathname.new(git_dir)
|
77
77
|
end
|
78
78
|
end
|
data/lib/shipit/task_commands.rb
CHANGED
data/lib/shipit/version.rb
CHANGED
data/lib/shipit.rb
CHANGED
@@ -5,7 +5,7 @@ require 'state_machines-activerecord'
|
|
5
5
|
require 'validate_url'
|
6
6
|
require 'responders'
|
7
7
|
require 'explicit-parameters'
|
8
|
-
require '
|
8
|
+
require 'paquito'
|
9
9
|
|
10
10
|
require 'sass-rails'
|
11
11
|
require 'coffee-rails'
|
@@ -64,7 +64,8 @@ module Shipit
|
|
64
64
|
|
65
65
|
delegate :table_name_prefix, to: :secrets
|
66
66
|
|
67
|
-
attr_accessor :disable_api_authentication, :timeout_exit_codes, :deployment_checks
|
67
|
+
attr_accessor :disable_api_authentication, :timeout_exit_codes, :deployment_checks, :respect_bare_shipit_file,
|
68
|
+
:database_serializer
|
68
69
|
attr_writer(
|
69
70
|
:internal_hook_receivers,
|
70
71
|
:preferred_org_emails,
|
@@ -77,6 +78,9 @@ module Shipit
|
|
77
78
|
end
|
78
79
|
|
79
80
|
self.timeout_exit_codes = [].freeze
|
81
|
+
self.respect_bare_shipit_file = true
|
82
|
+
|
83
|
+
alias_method :respect_bare_shipit_file?, :respect_bare_shipit_file
|
80
84
|
|
81
85
|
def authentication_disabled?
|
82
86
|
ENV['SHIPIT_DISABLE_AUTH'].present?
|
@@ -104,6 +108,50 @@ module Shipit
|
|
104
108
|
)
|
105
109
|
end
|
106
110
|
|
111
|
+
module SafeJSON
|
112
|
+
class << self
|
113
|
+
def load(serial)
|
114
|
+
return nil if serial.nil?
|
115
|
+
# JSON.load is unsafe, we should use parse instead
|
116
|
+
JSON.parse(serial)
|
117
|
+
end
|
118
|
+
|
119
|
+
def dump(object)
|
120
|
+
JSON.dump(object)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
module TransitionalSerializer
|
126
|
+
SafeYAML = Paquito::SafeYAML.new(deprecated_classes: ["ActiveSupport::HashWithIndifferentAccess"])
|
127
|
+
|
128
|
+
class << self
|
129
|
+
def load(serial)
|
130
|
+
return if serial.nil?
|
131
|
+
|
132
|
+
JSON.parse(serial)
|
133
|
+
rescue JSON::ParserError
|
134
|
+
SafeYAML.load(serial)
|
135
|
+
end
|
136
|
+
|
137
|
+
def dump(object)
|
138
|
+
return if object.nil?
|
139
|
+
JSON.dump(object)
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
self.database_serializer = TransitionalSerializer
|
145
|
+
|
146
|
+
def serialized_column(attribute_name, type: nil, coder: nil)
|
147
|
+
column = Paquito::SerializedColumn.new(database_serializer, type, attribute_name: attribute_name)
|
148
|
+
if coder
|
149
|
+
Paquito.chain(coder, column)
|
150
|
+
else
|
151
|
+
column
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
107
155
|
def github(organization: github_default_organization)
|
108
156
|
# Backward compatibility
|
109
157
|
# nil signifies the single github app config schema is being used
|
@@ -153,7 +201,11 @@ module Shipit
|
|
153
201
|
end
|
154
202
|
|
155
203
|
def user_access_tokens_key
|
156
|
-
|
204
|
+
if secrets.user_access_tokens_key.present?
|
205
|
+
secrets.user_access_tokens_key
|
206
|
+
elsif secrets.secret_key_base
|
207
|
+
Digest::SHA256.digest("user_access_tokens_key" + secrets.secret_key_base)
|
208
|
+
end
|
157
209
|
end
|
158
210
|
|
159
211
|
def host
|
@@ -10,7 +10,7 @@ github_repository = ARGV[1]
|
|
10
10
|
def get_json(url)
|
11
11
|
uri = URI.parse(url)
|
12
12
|
response = Net::HTTP.get_response(uri)
|
13
|
-
versions = JSON.
|
13
|
+
versions = JSON.parse(response.body)
|
14
14
|
end
|
15
15
|
|
16
16
|
versions = get_json("https://rubygems.org/api/v1/versions/#{gem_name}.json")
|
@@ -96,6 +96,24 @@ module Shipit
|
|
96
96
|
assert_equal 'test', @stack.branch
|
97
97
|
end
|
98
98
|
|
99
|
+
test "#update updates the stack when nil deploy_url" do
|
100
|
+
@stack.update(deploy_url: nil)
|
101
|
+
@stack.update(continuous_deployment: true)
|
102
|
+
assert_nil @stack.deploy_url
|
103
|
+
assert @stack.continuous_deployment
|
104
|
+
|
105
|
+
patch :update, params: {
|
106
|
+
id: @stack.to_param,
|
107
|
+
continuous_deployment: false,
|
108
|
+
}
|
109
|
+
|
110
|
+
assert_response :ok
|
111
|
+
@stack.reload
|
112
|
+
|
113
|
+
assert_nil @stack.deploy_url
|
114
|
+
refute @stack.continuous_deployment
|
115
|
+
end
|
116
|
+
|
99
117
|
test "#index returns a list of stacks" do
|
100
118
|
stack = Stack.last
|
101
119
|
get :index
|
@@ -189,6 +207,13 @@ module Shipit
|
|
189
207
|
assert_response :forbidden
|
190
208
|
assert_json 'message', 'This operation requires the `write:stack` permission'
|
191
209
|
end
|
210
|
+
|
211
|
+
test "#refresh queues a GithubSyncJob" do
|
212
|
+
assert_enqueued_with(job: GithubSyncJob, args: [stack_id: @stack.id]) do
|
213
|
+
post :refresh, params: { id: @stack.to_param }
|
214
|
+
end
|
215
|
+
assert_response :accepted
|
216
|
+
end
|
192
217
|
end
|
193
218
|
end
|
194
219
|
end
|
@@ -6,6 +6,7 @@ module Shipit
|
|
6
6
|
class TasksControllerTest < ActionController::TestCase
|
7
7
|
setup do
|
8
8
|
@stack = shipit_stacks(:shipit)
|
9
|
+
@user = shipit_users(:walrus)
|
9
10
|
authenticate!
|
10
11
|
end
|
11
12
|
|
@@ -90,6 +91,61 @@ module Shipit
|
|
90
91
|
assert_response :conflict
|
91
92
|
assert_json 'message', 'A task is already running.'
|
92
93
|
end
|
94
|
+
|
95
|
+
test "#trigger fails when user does not have deploy permission" do
|
96
|
+
@client.permissions.delete('deploy:stack')
|
97
|
+
@client.save!
|
98
|
+
|
99
|
+
assert_no_difference 'Task.count' do
|
100
|
+
post :trigger, params: { stack_id: @stack.to_param, task_name: 'restart' }
|
101
|
+
end
|
102
|
+
|
103
|
+
assert_response :forbidden
|
104
|
+
assert_json 'message', 'This operation requires the `deploy:stack` permission'
|
105
|
+
end
|
106
|
+
|
107
|
+
test "#abort aborts the task" do
|
108
|
+
task = shipit_deploys(:shipit_running)
|
109
|
+
task.ping
|
110
|
+
|
111
|
+
put :abort, params: { stack_id: @stack.to_param, id: task.id }
|
112
|
+
|
113
|
+
assert_response :accepted
|
114
|
+
assert_equal 'aborting', task.reload.status
|
115
|
+
end
|
116
|
+
|
117
|
+
test "#abort sets `aborted_by` to the current user" do
|
118
|
+
task = shipit_deploys(:shipit_running)
|
119
|
+
task.ping
|
120
|
+
request.headers['X-Shipit-User'] = @user.login
|
121
|
+
|
122
|
+
put :abort, params: { stack_id: @stack.to_param, id: task.id }
|
123
|
+
|
124
|
+
assert_equal task.reload.aborted_by, @user
|
125
|
+
end
|
126
|
+
|
127
|
+
test "#abort responds with method_not_allowed if the task is not currently running" do
|
128
|
+
task = shipit_deploys(:shipit_aborted)
|
129
|
+
task.ping
|
130
|
+
put :abort, params: { stack_id: @stack.to_param, id: task.id }
|
131
|
+
|
132
|
+
assert_response :method_not_allowed
|
133
|
+
assert_json 'message', 'This task is not currently running.'
|
134
|
+
end
|
135
|
+
|
136
|
+
test "#abort fails when user does not have deploy permission" do
|
137
|
+
@client.permissions.delete('deploy:stack')
|
138
|
+
@client.save!
|
139
|
+
task = shipit_deploys(:shipit_running)
|
140
|
+
task.ping
|
141
|
+
|
142
|
+
assert_no_difference 'Task.count' do
|
143
|
+
put :abort, params: { stack_id: @stack.to_param, id: task.id }
|
144
|
+
end
|
145
|
+
|
146
|
+
assert_response :forbidden
|
147
|
+
assert_json 'message', 'This operation requires the `deploy:stack` permission'
|
148
|
+
end
|
93
149
|
end
|
94
150
|
end
|
95
151
|
end
|
@@ -36,6 +36,17 @@ module Shipit
|
|
36
36
|
assert_redirected_to '/github/auth/github?origin=http%3A%2F%2Ftest.host%2F'
|
37
37
|
end
|
38
38
|
|
39
|
+
test "users which require a fresh login are redirected" do
|
40
|
+
user = shipit_users(:walrus)
|
41
|
+
user.update!(github_access_token: 'some_legacy_value')
|
42
|
+
assert_predicate user, :requires_fresh_login?
|
43
|
+
|
44
|
+
get :index
|
45
|
+
|
46
|
+
assert_redirected_to '/github/auth/github?origin=http%3A%2F%2Ftest.host%2F'
|
47
|
+
assert_nil session[:user_id]
|
48
|
+
end
|
49
|
+
|
39
50
|
test "current_user must be a member of at least a Shipit.github_teams" do
|
40
51
|
session[:user_id] = shipit_users(:bob).id
|
41
52
|
Shipit.stubs(:github_teams).returns([shipit_teams(:cyclimse_cooks), shipit_teams(:shopify_developers)])
|
data/test/dummy/db/schema.rb
CHANGED
@@ -10,7 +10,7 @@
|
|
10
10
|
#
|
11
11
|
# It's strongly recommended that you check this file into your version control system.
|
12
12
|
|
13
|
-
ActiveRecord::Schema.define(version:
|
13
|
+
ActiveRecord::Schema.define(version: 2021_11_03_154121) do
|
14
14
|
|
15
15
|
create_table "api_clients", force: :cascade do |t|
|
16
16
|
t.text "permissions", limit: 65535
|
@@ -315,7 +315,7 @@ ActiveRecord::Schema.define(version: 2021_08_23_075617) do
|
|
315
315
|
create_table "teams", force: :cascade do |t|
|
316
316
|
t.integer "github_id", limit: 4
|
317
317
|
t.string "api_url", limit: 255
|
318
|
-
t.string "slug", limit:
|
318
|
+
t.string "slug", limit: 255
|
319
319
|
t.string "name", limit: 255
|
320
320
|
t.string "organization", limit: 39
|
321
321
|
t.datetime "created_at", null: false
|
@@ -7,7 +7,7 @@ second_pending_travis:
|
|
7
7
|
conclusion: success
|
8
8
|
html_url: "http://www.example.com/run/424242"
|
9
9
|
details_url: "http://www.example.com/build/424242"
|
10
|
-
created_at: <%= 10.days.ago.
|
10
|
+
created_at: <%= 10.days.ago.to_formatted_s(:db) %>
|
11
11
|
|
12
12
|
check_runs_first_pending_coveralls:
|
13
13
|
stack: check_runs
|
@@ -15,7 +15,7 @@ check_runs_first_pending_coveralls:
|
|
15
15
|
github_id: 43
|
16
16
|
title: lets go
|
17
17
|
name: Coverage metrics
|
18
|
-
created_at: <%= 10.days.ago.
|
18
|
+
created_at: <%= 10.days.ago.to_formatted_s(:db) %>
|
19
19
|
conclusion: pending
|
20
20
|
html_url: "http://www.example.com/run/434343"
|
21
21
|
details_url: "http://www.example.com/build/434343"
|
@@ -26,7 +26,7 @@ check_runs_first_success_coveralls:
|
|
26
26
|
github_id: 434343
|
27
27
|
title: lets go
|
28
28
|
name: Coverage metrics
|
29
|
-
created_at: <%= 9.days.ago.
|
29
|
+
created_at: <%= 9.days.ago.to_formatted_s(:db) %>
|
30
30
|
conclusion: success
|
31
31
|
html_url: "http://www.example.com/run/434343"
|
32
32
|
details_url: "http://www.example.com/build/434343"
|