carson 3.27.0 → 3.28.0
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/.github/workflows/carson_policy.yml +1 -1
- data/API.md +34 -7
- data/MANUAL.md +10 -9
- data/README.md +15 -8
- data/RELEASE.md +27 -1
- data/VERSION +1 -1
- data/carson.gemspec +1 -0
- data/lib/carson/delivery.rb +9 -2
- data/lib/carson/ledger.rb +318 -34
- data/lib/carson/runtime/deliver.rb +779 -85
- data/lib/carson/runtime/govern.rb +118 -66
- data/lib/carson/runtime/local/merge_proof.rb +199 -0
- data/lib/carson/runtime/local/sync.rb +89 -0
- data/lib/carson/runtime/local/worktree.rb +7 -21
- data/lib/carson/runtime/local.rb +1 -0
- data/lib/carson/runtime/status.rb +34 -1
- data/lib/carson/worktree.rb +95 -18
- metadata +24 -3
data/lib/carson/ledger.rb
CHANGED
|
@@ -7,36 +7,40 @@ module Carson
|
|
|
7
7
|
class Ledger
|
|
8
8
|
UNSET = Object.new
|
|
9
9
|
ACTIVE_DELIVERY_STATES = Delivery::ACTIVE_STATES
|
|
10
|
+
SQLITE_HEADER = "SQLite format 3\0".b.freeze
|
|
10
11
|
|
|
11
12
|
def initialize( path: )
|
|
12
13
|
@path = File.expand_path( path )
|
|
13
14
|
FileUtils.mkdir_p( File.dirname( @path ) )
|
|
15
|
+
migrate_legacy_state_if_needed!
|
|
14
16
|
end
|
|
15
17
|
|
|
16
18
|
attr_reader :path
|
|
17
19
|
|
|
18
20
|
# Creates or refreshes a delivery for the same branch head.
|
|
19
|
-
def upsert_delivery(
|
|
21
|
+
def upsert_delivery(
|
|
22
|
+
repository:, branch_name:, head:, worktree_path:, pr_number:, pr_url:, status:, summary:, cause:,
|
|
23
|
+
pull_request_state: nil, pull_request_draft: nil, pull_request_merged_at: nil, merge_proof: nil
|
|
24
|
+
)
|
|
20
25
|
timestamp = now_utc
|
|
21
26
|
|
|
22
27
|
with_state do |state|
|
|
28
|
+
repo_paths = repo_identity_paths( repo_path: repository.path )
|
|
29
|
+
matches = matching_deliveries(
|
|
30
|
+
state: state,
|
|
31
|
+
repo_paths: repo_paths,
|
|
32
|
+
branch_name: branch_name,
|
|
33
|
+
head: head
|
|
34
|
+
)
|
|
23
35
|
key = delivery_key( repo_path: repository.path, branch_name: branch_name, head: head )
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
existing[ "worktree_path" ] = worktree_path
|
|
29
|
-
existing[ "status" ] = status
|
|
30
|
-
existing[ "pr_number" ] = pr_number
|
|
31
|
-
existing[ "pr_url" ] = pr_url
|
|
32
|
-
existing[ "cause" ] = cause
|
|
33
|
-
existing[ "summary" ] = summary
|
|
34
|
-
existing[ "updated_at" ] = timestamp
|
|
35
|
-
return build_delivery( key: key, data: existing, repository: repository )
|
|
36
|
-
end
|
|
36
|
+
sequence = matches.map { |_existing_key, data| delivery_sequence( data: data ) }.compact.min
|
|
37
|
+
created_at = matches.map { |_existing_key, data| data.fetch( "created_at", "" ).to_s }.reject( &:empty? ).min || timestamp
|
|
38
|
+
revisions = merged_revisions( entries: matches )
|
|
39
|
+
matches.each { |existing_key, _data| state[ "deliveries" ].delete( existing_key ) }
|
|
37
40
|
|
|
38
41
|
supersede_branch!( state: state, repo_path: repository.path, branch_name: branch_name, timestamp: timestamp )
|
|
39
42
|
state[ "deliveries" ][ key ] = {
|
|
43
|
+
"sequence" => sequence || next_delivery_sequence!( state: state ),
|
|
40
44
|
"repo_path" => repository.path,
|
|
41
45
|
"branch_name" => branch_name,
|
|
42
46
|
"head" => head,
|
|
@@ -44,13 +48,17 @@ module Carson
|
|
|
44
48
|
"status" => status,
|
|
45
49
|
"pr_number" => pr_number,
|
|
46
50
|
"pr_url" => pr_url,
|
|
51
|
+
"pull_request_state" => pull_request_state,
|
|
52
|
+
"pull_request_draft" => pull_request_draft,
|
|
53
|
+
"pull_request_merged_at" => pull_request_merged_at,
|
|
54
|
+
"merge_proof" => serialise_merge_proof( merge_proof: merge_proof ),
|
|
47
55
|
"cause" => cause,
|
|
48
56
|
"summary" => summary,
|
|
49
|
-
"created_at" =>
|
|
57
|
+
"created_at" => created_at,
|
|
50
58
|
"updated_at" => timestamp,
|
|
51
59
|
"integrated_at" => nil,
|
|
52
60
|
"superseded_at" => nil,
|
|
53
|
-
"revisions" =>
|
|
61
|
+
"revisions" => revisions
|
|
54
62
|
}
|
|
55
63
|
build_delivery( key: key, data: state[ "deliveries" ][ key ], repository: repository )
|
|
56
64
|
end
|
|
@@ -69,7 +77,23 @@ module Carson
|
|
|
69
77
|
|
|
70
78
|
return nil if candidates.empty?
|
|
71
79
|
|
|
72
|
-
key, data = candidates.max_by { |k, d| [ d[ "updated_at" ].to_s, k ] }
|
|
80
|
+
key, data = candidates.max_by { |k, d| [ d[ "updated_at" ].to_s, delivery_sequence( data: d ), k ] }
|
|
81
|
+
build_delivery( key: key, data: data )
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Looks up the newest delivery for a branch across active and terminal states.
|
|
85
|
+
def latest_delivery( repo_path:, branch_name: )
|
|
86
|
+
state = load_state
|
|
87
|
+
repo_paths = repo_identity_paths( repo_path: repo_path )
|
|
88
|
+
|
|
89
|
+
candidates = state[ "deliveries" ].select do |_key, data|
|
|
90
|
+
repo_paths.include?( data[ "repo_path" ] ) &&
|
|
91
|
+
data[ "branch_name" ] == branch_name
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
return nil if candidates.empty?
|
|
95
|
+
|
|
96
|
+
key, data = candidates.max_by { |k, d| [ d[ "updated_at" ].to_s, delivery_sequence( data: d ), k ] }
|
|
73
97
|
build_delivery( key: key, data: data )
|
|
74
98
|
end
|
|
75
99
|
|
|
@@ -80,7 +104,7 @@ module Carson
|
|
|
80
104
|
|
|
81
105
|
state[ "deliveries" ]
|
|
82
106
|
.select { |_key, data| repo_paths.include?( data[ "repo_path" ] ) && ACTIVE_DELIVERY_STATES.include?( data[ "status" ] ) }
|
|
83
|
-
.sort_by { |key, data| [ data
|
|
107
|
+
.sort_by { |key, data| [ delivery_sequence( data: data ), key ] }
|
|
84
108
|
.map { |key, data| build_delivery( key: key, data: data ) }
|
|
85
109
|
end
|
|
86
110
|
|
|
@@ -95,7 +119,7 @@ module Carson
|
|
|
95
119
|
data[ "status" ] == "integrated" &&
|
|
96
120
|
!data[ "worktree_path" ].to_s.strip.empty?
|
|
97
121
|
end
|
|
98
|
-
.sort_by { |key, data| [ data[ "integrated_at" ].to_s, data
|
|
122
|
+
.sort_by { |key, data| [ data[ "integrated_at" ].to_s, delivery_sequence( data: data ), key ] }
|
|
99
123
|
.map { |key, data| build_delivery( key: key, data: data ) }
|
|
100
124
|
end
|
|
101
125
|
|
|
@@ -105,6 +129,10 @@ module Carson
|
|
|
105
129
|
status: UNSET,
|
|
106
130
|
pr_number: UNSET,
|
|
107
131
|
pr_url: UNSET,
|
|
132
|
+
pull_request_state: UNSET,
|
|
133
|
+
pull_request_draft: UNSET,
|
|
134
|
+
pull_request_merged_at: UNSET,
|
|
135
|
+
merge_proof: UNSET,
|
|
108
136
|
cause: UNSET,
|
|
109
137
|
summary: UNSET,
|
|
110
138
|
worktree_path: UNSET,
|
|
@@ -112,12 +140,15 @@ module Carson
|
|
|
112
140
|
superseded_at: UNSET
|
|
113
141
|
)
|
|
114
142
|
with_state do |state|
|
|
115
|
-
data = state
|
|
116
|
-
raise "delivery not found: #{delivery.key}" unless data
|
|
143
|
+
key, data = resolve_delivery_entry( state: state, delivery: delivery )
|
|
117
144
|
|
|
118
145
|
data[ "status" ] = status unless status.equal?( UNSET )
|
|
119
146
|
data[ "pr_number" ] = pr_number unless pr_number.equal?( UNSET )
|
|
120
147
|
data[ "pr_url" ] = pr_url unless pr_url.equal?( UNSET )
|
|
148
|
+
data[ "pull_request_state" ] = pull_request_state unless pull_request_state.equal?( UNSET )
|
|
149
|
+
data[ "pull_request_draft" ] = pull_request_draft unless pull_request_draft.equal?( UNSET )
|
|
150
|
+
data[ "pull_request_merged_at" ] = pull_request_merged_at unless pull_request_merged_at.equal?( UNSET )
|
|
151
|
+
data[ "merge_proof" ] = serialise_merge_proof( merge_proof: merge_proof ) unless merge_proof.equal?( UNSET )
|
|
121
152
|
data[ "cause" ] = cause unless cause.equal?( UNSET )
|
|
122
153
|
data[ "summary" ] = summary unless summary.equal?( UNSET )
|
|
123
154
|
data[ "worktree_path" ] = worktree_path unless worktree_path.equal?( UNSET )
|
|
@@ -125,7 +156,7 @@ module Carson
|
|
|
125
156
|
data[ "superseded_at" ] = superseded_at unless superseded_at.equal?( UNSET )
|
|
126
157
|
data[ "updated_at" ] = now_utc
|
|
127
158
|
|
|
128
|
-
build_delivery( key:
|
|
159
|
+
build_delivery( key: key, data: data, repository: delivery.repository )
|
|
129
160
|
end
|
|
130
161
|
end
|
|
131
162
|
|
|
@@ -134,8 +165,7 @@ module Carson
|
|
|
134
165
|
timestamp = now_utc
|
|
135
166
|
|
|
136
167
|
with_state do |state|
|
|
137
|
-
data = state
|
|
138
|
-
raise "delivery not found: #{delivery.key}" unless data
|
|
168
|
+
_key, data = resolve_delivery_entry( state: state, delivery: delivery )
|
|
139
169
|
|
|
140
170
|
revisions = data[ "revisions" ] ||= []
|
|
141
171
|
next_number = ( revisions.map { |r| r[ "number" ].to_i }.max || 0 ) + 1
|
|
@@ -166,11 +196,7 @@ module Carson
|
|
|
166
196
|
|
|
167
197
|
# Acquires file lock, loads state, yields for mutation, saves atomically, releases lock.
|
|
168
198
|
def with_state
|
|
169
|
-
|
|
170
|
-
FileUtils.mkdir_p( File.dirname( lock_path ) )
|
|
171
|
-
FileUtils.touch( lock_path )
|
|
172
|
-
|
|
173
|
-
File.open( lock_path, File::RDWR | File::CREAT ) do |lock_file|
|
|
199
|
+
with_state_lock do |lock_file|
|
|
174
200
|
lock_file.flock( File::LOCK_EX )
|
|
175
201
|
state = load_state
|
|
176
202
|
result = yield state
|
|
@@ -180,21 +206,22 @@ module Carson
|
|
|
180
206
|
end
|
|
181
207
|
|
|
182
208
|
def load_state
|
|
183
|
-
|
|
209
|
+
return { "deliveries" => {}, "recovery_events" => [] } unless File.exist?( path )
|
|
184
210
|
|
|
185
|
-
raw = File.
|
|
211
|
+
raw = File.binread( path )
|
|
186
212
|
return { "deliveries" => {}, "recovery_events" => [] } if raw.strip.empty?
|
|
187
213
|
|
|
188
214
|
parsed = JSON.parse( raw )
|
|
189
215
|
raise "state file must contain a JSON object at #{path}" unless parsed.is_a?( Hash )
|
|
190
216
|
parsed[ "deliveries" ] ||= {}
|
|
191
|
-
|
|
217
|
+
normalise_state!( state: parsed )
|
|
192
218
|
parsed
|
|
193
|
-
rescue JSON::ParserError => exception
|
|
219
|
+
rescue JSON::ParserError, Encoding::InvalidByteSequenceError, Encoding::UndefinedConversionError => exception
|
|
194
220
|
raise "invalid JSON in state file #{path}: #{exception.message}"
|
|
195
221
|
end
|
|
196
222
|
|
|
197
223
|
def save_state!( state )
|
|
224
|
+
normalise_state!( state: state )
|
|
198
225
|
tmp_path = "#{path}.tmp"
|
|
199
226
|
File.write( tmp_path, JSON.pretty_generate( state ) + "\n" )
|
|
200
227
|
File.rename( tmp_path, path )
|
|
@@ -218,6 +245,10 @@ module Carson
|
|
|
218
245
|
status: data.fetch( "status" ),
|
|
219
246
|
pull_request_number: data[ "pr_number" ],
|
|
220
247
|
pull_request_url: data[ "pr_url" ],
|
|
248
|
+
pull_request_state: data[ "pull_request_state" ],
|
|
249
|
+
pull_request_draft: data[ "pull_request_draft" ],
|
|
250
|
+
pull_request_merged_at: data[ "pull_request_merged_at" ],
|
|
251
|
+
merge_proof: deserialise_merge_proof( merge_proof: data[ "merge_proof" ] ),
|
|
221
252
|
revisions: revisions,
|
|
222
253
|
cause: data[ "cause" ],
|
|
223
254
|
summary: data[ "summary" ],
|
|
@@ -242,6 +273,233 @@ module Carson
|
|
|
242
273
|
)
|
|
243
274
|
end
|
|
244
275
|
|
|
276
|
+
def migrate_legacy_state_if_needed!
|
|
277
|
+
with_state_lock do |lock_file|
|
|
278
|
+
lock_file.flock( File::LOCK_EX )
|
|
279
|
+
source_path = legacy_sqlite_source_path
|
|
280
|
+
next unless source_path
|
|
281
|
+
|
|
282
|
+
state = load_legacy_sqlite_state( path: source_path )
|
|
283
|
+
save_state!( state )
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
def with_state_lock
|
|
288
|
+
lock_path = "#{path}.lock"
|
|
289
|
+
FileUtils.mkdir_p( File.dirname( lock_path ) )
|
|
290
|
+
FileUtils.touch( lock_path )
|
|
291
|
+
|
|
292
|
+
File.open( lock_path, File::RDWR | File::CREAT ) do |lock_file|
|
|
293
|
+
yield lock_file
|
|
294
|
+
end
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
def legacy_sqlite_source_path
|
|
298
|
+
return nil unless state_path_requires_migration?
|
|
299
|
+
return path if sqlite_database_file?( path: path )
|
|
300
|
+
|
|
301
|
+
legacy_path = legacy_state_path
|
|
302
|
+
return nil unless legacy_path
|
|
303
|
+
return legacy_path if sqlite_database_file?( path: legacy_path )
|
|
304
|
+
|
|
305
|
+
nil
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
def state_path_requires_migration?
|
|
309
|
+
return true if sqlite_database_file?( path: path )
|
|
310
|
+
return false if File.exist?( path )
|
|
311
|
+
!legacy_state_path.nil?
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
def legacy_state_path
|
|
315
|
+
return nil unless path.end_with?( ".json" )
|
|
316
|
+
path.sub( /\.json\z/, ".sqlite3" )
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
def sqlite_database_file?( path: )
|
|
320
|
+
return false unless File.file?( path )
|
|
321
|
+
File.binread( path, SQLITE_HEADER.bytesize ) == SQLITE_HEADER
|
|
322
|
+
rescue StandardError
|
|
323
|
+
false
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
def load_legacy_sqlite_state( path: )
|
|
327
|
+
begin
|
|
328
|
+
require "sqlite3"
|
|
329
|
+
rescue LoadError => exception
|
|
330
|
+
raise "legacy SQLite ledger found at #{path}, but sqlite3 support is unavailable: #{exception.message}"
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
database = open_legacy_sqlite_database( path: path )
|
|
334
|
+
deliveries = database.execute( "SELECT * FROM deliveries ORDER BY id ASC" )
|
|
335
|
+
revisions_by_delivery = database.execute(
|
|
336
|
+
"SELECT * FROM revisions ORDER BY delivery_id ASC, number ASC, id ASC"
|
|
337
|
+
).group_by { |row| row.fetch( "delivery_id" ) }
|
|
338
|
+
|
|
339
|
+
state = {
|
|
340
|
+
"deliveries" => {},
|
|
341
|
+
"recovery_events" => [],
|
|
342
|
+
"next_sequence" => 1
|
|
343
|
+
}
|
|
344
|
+
deliveries.each do |row|
|
|
345
|
+
key = delivery_key(
|
|
346
|
+
repo_path: row.fetch( "repo_path" ),
|
|
347
|
+
branch_name: row.fetch( "branch_name" ),
|
|
348
|
+
head: row.fetch( "head" )
|
|
349
|
+
)
|
|
350
|
+
state[ "deliveries" ][ key ] = {
|
|
351
|
+
"sequence" => row.fetch( "id" ).to_i,
|
|
352
|
+
"repo_path" => row.fetch( "repo_path" ),
|
|
353
|
+
"branch_name" => row.fetch( "branch_name" ),
|
|
354
|
+
"head" => row.fetch( "head" ),
|
|
355
|
+
"worktree_path" => row.fetch( "worktree_path" ),
|
|
356
|
+
"status" => row.fetch( "status" ),
|
|
357
|
+
"pr_number" => row.fetch( "pr_number" ),
|
|
358
|
+
"pr_url" => row.fetch( "pr_url" ),
|
|
359
|
+
"pull_request_state" => nil,
|
|
360
|
+
"pull_request_draft" => nil,
|
|
361
|
+
"pull_request_merged_at" => nil,
|
|
362
|
+
"merge_proof" => nil,
|
|
363
|
+
"cause" => row.fetch( "cause" ),
|
|
364
|
+
"summary" => row.fetch( "summary" ),
|
|
365
|
+
"created_at" => row.fetch( "created_at" ),
|
|
366
|
+
"updated_at" => row.fetch( "updated_at" ),
|
|
367
|
+
"integrated_at" => row.fetch( "integrated_at" ),
|
|
368
|
+
"superseded_at" => row.fetch( "superseded_at" ),
|
|
369
|
+
"revisions" => Array( revisions_by_delivery[ row.fetch( "id" ) ] ).map do |revision|
|
|
370
|
+
{
|
|
371
|
+
"number" => revision.fetch( "number" ).to_i,
|
|
372
|
+
"cause" => revision.fetch( "cause" ),
|
|
373
|
+
"provider" => revision.fetch( "provider" ),
|
|
374
|
+
"status" => revision.fetch( "status" ),
|
|
375
|
+
"started_at" => revision.fetch( "started_at" ),
|
|
376
|
+
"finished_at" => revision.fetch( "finished_at" ),
|
|
377
|
+
"summary" => revision.fetch( "summary" )
|
|
378
|
+
}
|
|
379
|
+
end
|
|
380
|
+
}
|
|
381
|
+
end
|
|
382
|
+
normalise_state!( state: state )
|
|
383
|
+
state
|
|
384
|
+
ensure
|
|
385
|
+
database&.close
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
def open_legacy_sqlite_database( path: )
|
|
389
|
+
database = SQLite3::Database.new( "file:#{path}?immutable=1", readonly: true, uri: true )
|
|
390
|
+
database.results_as_hash = true
|
|
391
|
+
database.busy_timeout = 5_000
|
|
392
|
+
database
|
|
393
|
+
rescue SQLite3::CantOpenException
|
|
394
|
+
database&.close
|
|
395
|
+
database = SQLite3::Database.new( path, readonly: true )
|
|
396
|
+
database.results_as_hash = true
|
|
397
|
+
database.busy_timeout = 5_000
|
|
398
|
+
database
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
def normalise_state!( state: )
|
|
402
|
+
deliveries = state[ "deliveries" ]
|
|
403
|
+
raise "state file must contain a JSON object at #{path}" unless deliveries.is_a?( Hash )
|
|
404
|
+
state[ "recovery_events" ] = Array( state[ "recovery_events" ] )
|
|
405
|
+
|
|
406
|
+
sequence_counts = Hash.new( 0 )
|
|
407
|
+
deliveries.each_value do |data|
|
|
408
|
+
data[ "revisions" ] = Array( data[ "revisions" ] )
|
|
409
|
+
data[ "merge_proof" ] = serialise_merge_proof( merge_proof: data[ "merge_proof" ] ) if data.key?( "merge_proof" )
|
|
410
|
+
sequence = integer_or_nil( value: data[ "sequence" ] )
|
|
411
|
+
sequence_counts[ sequence ] += 1 unless sequence.nil? || sequence <= 0
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
max_sequence = sequence_counts.keys.max.to_i
|
|
415
|
+
next_sequence = max_sequence + 1
|
|
416
|
+
deliveries.keys.sort_by { |key| [ deliveries.fetch( key ).fetch( "created_at", "" ).to_s, key ] }.each do |key|
|
|
417
|
+
data = deliveries.fetch( key )
|
|
418
|
+
sequence = integer_or_nil( value: data[ "sequence" ] )
|
|
419
|
+
if sequence.nil? || sequence <= 0 || sequence_counts[ sequence ] > 1
|
|
420
|
+
sequence = next_sequence
|
|
421
|
+
next_sequence += 1
|
|
422
|
+
end
|
|
423
|
+
data[ "sequence" ] = sequence
|
|
424
|
+
end
|
|
425
|
+
|
|
426
|
+
recorded_next = integer_or_nil( value: state[ "next_sequence" ] ) || 1
|
|
427
|
+
state[ "next_sequence" ] = [ recorded_next, next_sequence ].max
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
def next_delivery_sequence!( state: )
|
|
431
|
+
sequence = integer_or_nil( value: state[ "next_sequence" ] ) || 1
|
|
432
|
+
state[ "next_sequence" ] = sequence + 1
|
|
433
|
+
sequence
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
def integer_or_nil( value: )
|
|
437
|
+
Integer( value )
|
|
438
|
+
rescue ArgumentError, TypeError
|
|
439
|
+
nil
|
|
440
|
+
end
|
|
441
|
+
|
|
442
|
+
def delivery_sequence( data: )
|
|
443
|
+
integer_or_nil( value: data[ "sequence" ] ) || 0
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
def matching_deliveries( state:, repo_paths:, branch_name:, head: UNSET )
|
|
447
|
+
state[ "deliveries" ].select do |_key, data|
|
|
448
|
+
next false unless repo_paths.include?( data[ "repo_path" ] )
|
|
449
|
+
next false unless data[ "branch_name" ] == branch_name
|
|
450
|
+
next false unless head.equal?( UNSET ) || data[ "head" ] == head
|
|
451
|
+
|
|
452
|
+
true
|
|
453
|
+
end
|
|
454
|
+
end
|
|
455
|
+
|
|
456
|
+
def resolve_delivery_entry( state:, delivery: )
|
|
457
|
+
data = state[ "deliveries" ][ delivery.key ]
|
|
458
|
+
return [ delivery.key, data ] if data
|
|
459
|
+
|
|
460
|
+
repo_paths = repo_identity_paths( repo_path: delivery.repo_path )
|
|
461
|
+
match = matching_deliveries(
|
|
462
|
+
state: state,
|
|
463
|
+
repo_paths: repo_paths,
|
|
464
|
+
branch_name: delivery.branch,
|
|
465
|
+
head: delivery.head
|
|
466
|
+
).max_by { |key, row| [ row[ "updated_at" ].to_s, delivery_sequence( data: row ), key ] }
|
|
467
|
+
raise "delivery not found: #{delivery.key}" unless match
|
|
468
|
+
|
|
469
|
+
match
|
|
470
|
+
end
|
|
471
|
+
|
|
472
|
+
def merged_revisions( entries: )
|
|
473
|
+
entries
|
|
474
|
+
.flat_map { |_key, data| Array( data[ "revisions" ] ) }
|
|
475
|
+
.map do |revision|
|
|
476
|
+
{
|
|
477
|
+
"number" => revision.fetch( "number", 0 ).to_i,
|
|
478
|
+
"cause" => revision[ "cause" ],
|
|
479
|
+
"provider" => revision[ "provider" ],
|
|
480
|
+
"status" => revision[ "status" ],
|
|
481
|
+
"started_at" => revision[ "started_at" ],
|
|
482
|
+
"finished_at" => revision[ "finished_at" ],
|
|
483
|
+
"summary" => revision[ "summary" ]
|
|
484
|
+
}
|
|
485
|
+
end
|
|
486
|
+
.uniq do |revision|
|
|
487
|
+
[
|
|
488
|
+
revision[ "cause" ],
|
|
489
|
+
revision[ "provider" ],
|
|
490
|
+
revision[ "status" ],
|
|
491
|
+
revision[ "started_at" ],
|
|
492
|
+
revision[ "finished_at" ],
|
|
493
|
+
revision[ "summary" ]
|
|
494
|
+
]
|
|
495
|
+
end
|
|
496
|
+
.sort_by { |revision| [ revision.fetch( "started_at", "" ).to_s, revision.fetch( "number", 0 ).to_i ] }
|
|
497
|
+
.each_with_index
|
|
498
|
+
.map do |revision, index|
|
|
499
|
+
revision.merge( "number" => index + 1 )
|
|
500
|
+
end
|
|
501
|
+
end
|
|
502
|
+
|
|
245
503
|
def supersede_branch!( state:, repo_path:, branch_name:, timestamp: )
|
|
246
504
|
repo_paths = repo_identity_paths( repo_path: repo_path )
|
|
247
505
|
state[ "deliveries" ].each do |_key, data|
|
|
@@ -293,8 +551,34 @@ module Carson
|
|
|
293
551
|
nil
|
|
294
552
|
end
|
|
295
553
|
|
|
554
|
+
def serialise_merge_proof( merge_proof: )
|
|
555
|
+
return nil unless merge_proof.is_a?( Hash )
|
|
556
|
+
|
|
557
|
+
{
|
|
558
|
+
"applicable" => merge_proof[ :applicable ].nil? ? merge_proof[ "applicable" ] : merge_proof[ :applicable ],
|
|
559
|
+
"proven" => merge_proof[ :proven ].nil? ? merge_proof[ "proven" ] : merge_proof[ :proven ],
|
|
560
|
+
"basis" => merge_proof[ :basis ] || merge_proof[ "basis" ],
|
|
561
|
+
"summary" => merge_proof[ :summary ] || merge_proof[ "summary" ],
|
|
562
|
+
"main_branch" => merge_proof[ :main_branch ] || merge_proof[ "main_branch" ],
|
|
563
|
+
"changed_files_count" => ( merge_proof[ :changed_files_count ] || merge_proof[ "changed_files_count" ] || 0 ).to_i
|
|
564
|
+
}
|
|
565
|
+
end
|
|
566
|
+
|
|
567
|
+
def deserialise_merge_proof( merge_proof: )
|
|
568
|
+
return nil unless merge_proof.is_a?( Hash )
|
|
569
|
+
|
|
570
|
+
{
|
|
571
|
+
applicable: merge_proof[ "applicable" ],
|
|
572
|
+
proven: merge_proof[ "proven" ],
|
|
573
|
+
basis: merge_proof[ "basis" ],
|
|
574
|
+
summary: merge_proof[ "summary" ],
|
|
575
|
+
main_branch: merge_proof[ "main_branch" ],
|
|
576
|
+
changed_files_count: merge_proof.fetch( "changed_files_count", 0 ).to_i
|
|
577
|
+
}
|
|
578
|
+
end
|
|
579
|
+
|
|
296
580
|
def now_utc
|
|
297
|
-
Time.now.utc.iso8601
|
|
581
|
+
Time.now.utc.iso8601( 6 )
|
|
298
582
|
end
|
|
299
583
|
|
|
300
584
|
def record_recovery_event( repository:, branch_name:, pr_number:, pr_url:, check_name:, default_branch:, default_branch_sha:, pr_sha:, actor:, merge_method:, status:, summary: )
|