travis-backup 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +9 -5
- data/lib/backup/move_logs.rb +37 -0
- data/lib/backup/remove_old.rb +204 -0
- data/lib/backup/remove_orphans.rb +146 -0
- data/lib/config.rb +3 -3
- data/lib/db_helper.rb +27 -0
- data/lib/dry_run_reporter.rb +33 -0
- data/lib/models/build.rb +1 -0
- data/lib/models/repository.rb +1 -1
- data/lib/travis-backup.rb +13 -263
- data/travis-backup.gemspec +2 -1
- metadata +21 -3
- data/Gemfile.lock +0 -212
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 85a9fa54e0b9f6637f1371e29d72682ff808bb2a1c7ee5e8d9432634f2961f14
|
4
|
+
data.tar.gz: 96f242b4475d5b50f5a46bfb0e2aaac618aad78a7107a89977d4e5ffde103eba
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 343866ae70df1f9f7c5fccdbc49f350c4d58ff195c9c527dd13995f3a66710b09ac53e269ab03eacaa36e6c0a17004d58635aec7c5ce0f7fce5d44c1029282de
|
7
|
+
data.tar.gz: f6f2a7d430da5bdfb6ece3de27c4c7201061071f6636e4d16f7dc3b70f04b922d1d0e80b87c78bdec33bdf42fa267c7d46ed733dc91eaccb8d094ec1e7005c22
|
data/README.md
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# README
|
2
2
|
|
3
|
-
*travis-backup* is an application that helps with housekeeping and backup for Travis CI database v2.2 and with migration to v3.0 database.
|
3
|
+
*travis-backup* is an application that helps with housekeeping and backup for Travis CI database v2.2 and with migration to v3.0 database. By default it removes requests and builds with their corresponding jobs and logs, as long as they are older than given threshold says (and backups them in files, if this option is active). Although it can be also run with special modes: `move_logs`, for moving logs from one database to another, and `remove_orphans`, for deleting all orphaned data.
|
4
4
|
|
5
5
|
### Installation and run
|
6
6
|
|
@@ -75,15 +75,19 @@ backup:
|
|
75
75
|
limit: 1000 # builds limit for one backup file
|
76
76
|
threshold: 6 # number of months from now - data younger than this time won't be backuped
|
77
77
|
files_location: './dump' # path of the folder in which backup files will be placed
|
78
|
-
user_id
|
79
|
-
org_id
|
80
|
-
repo_id
|
78
|
+
user_id: 1 # run only for given user
|
79
|
+
org_id: 1 # run only for given organization
|
80
|
+
repo_id: 1 # run only for given repository
|
81
|
+
move_logs: false # run in move logs mode - move all logs to database at destination_db_url URL
|
82
|
+
remove_orphans: false # run in remove orphans mode
|
81
83
|
```
|
82
84
|
|
83
|
-
You can also set these properties using env vars corresponding to them: `IF_BACKUP`, `BACKUP_DRY_RUN`, `BACKUP_LIMIT`, `BACKUP_THRESHOLD`, `BACKUP_FILES_LOCATION`, `
|
85
|
+
You can also set these properties using env vars corresponding to them: `IF_BACKUP`, `BACKUP_DRY_RUN`, `BACKUP_LIMIT`, `BACKUP_THRESHOLD`, `BACKUP_FILES_LOCATION`, `BACKUP_USER_ID`, `BACKUP_ORG_ID`, `BACKUP_REPO_ID`, `BACKUP_MOVE_LOGS`, `BACKUP_REMOVE_ORPHANS`.
|
84
86
|
|
85
87
|
You should also specify your database url. You can do this the standard way in `config/database.yml` file, setting the `database_url` hash argument while creating `Backup` instance or using the `DATABASE_URL` env var. Your database should be consistent with the Travis 2.2 database schema.
|
86
88
|
|
89
|
+
For `move_logs` mode you need also to specify a destination database. You can set it also in `config/database.yml` file, in `destination` subsection, setting the `destination_db_url` hash argument while creating `Backup` instance or using the `BACKUP_DESTINATION_DB_URL` env var. Your destination database should be consistent with the Travis 3.0 database schema.
|
90
|
+
|
87
91
|
### How to run the test suite
|
88
92
|
|
89
93
|
You can run the test after cloning this repository. Next you should call
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Backup
|
4
|
+
class MoveLogs
|
5
|
+
attr_reader :config
|
6
|
+
|
7
|
+
def initialize(config, db_helper, dry_run_reporter=nil)
|
8
|
+
@config = config
|
9
|
+
@dry_run_reporter = dry_run_reporter
|
10
|
+
@db_helper = db_helper
|
11
|
+
end
|
12
|
+
|
13
|
+
def run
|
14
|
+
return run_dry if @config.dry_run
|
15
|
+
|
16
|
+
@db_helper.connect_db(@config.database_url)
|
17
|
+
Log.order(:id).in_batches(of: @config.limit.to_i).map do |logs_batch|
|
18
|
+
log_hashes = logs_batch.as_json
|
19
|
+
@db_helper.connect_db(@config.destination_db_url)
|
20
|
+
|
21
|
+
log_hashes.each do |log_hash|
|
22
|
+
new_log = Log.new(log_hash)
|
23
|
+
new_log.save!
|
24
|
+
end
|
25
|
+
|
26
|
+
@db_helper.connect_db(@config.database_url)
|
27
|
+
|
28
|
+
logs_batch.each(&:destroy)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def run_dry
|
33
|
+
ids = Log.order(:id).map(&:id)
|
34
|
+
@dry_run_reporter.add_to_report(:logs, *ids)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,204 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Backup
|
4
|
+
class RemoveOld
|
5
|
+
attr_reader :config
|
6
|
+
|
7
|
+
def initialize(config, dry_run_reporter=nil)
|
8
|
+
@config = config
|
9
|
+
@dry_run_reporter = dry_run_reporter
|
10
|
+
end
|
11
|
+
|
12
|
+
def dry_run_report
|
13
|
+
@dry_run_reporter.report
|
14
|
+
end
|
15
|
+
|
16
|
+
def run(args={})
|
17
|
+
user_id = args[:user_id] || @config.user_id
|
18
|
+
repo_id = args[:repo_id] || @config.repo_id
|
19
|
+
org_id = args[:org_id] || @config.org_id
|
20
|
+
|
21
|
+
if user_id
|
22
|
+
process_repos_for_owner(user_id, 'User')
|
23
|
+
elsif org_id
|
24
|
+
process_repos_for_owner(org_id, 'Organization')
|
25
|
+
elsif repo_id
|
26
|
+
process_repo_with_id(repo_id)
|
27
|
+
else
|
28
|
+
process_all_repos
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def process_repos_for_owner(owner_id, owner_type)
|
33
|
+
Repository.where('owner_id = ? and owner_type = ?', owner_id, owner_type).order(:id).each do |repository|
|
34
|
+
process_repo(repository)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def process_repo_with_id(repo_id)
|
39
|
+
process_repo(Repository.find(repo_id))
|
40
|
+
end
|
41
|
+
|
42
|
+
def process_all_repos
|
43
|
+
Repository.order(:id).each do |repository|
|
44
|
+
process_repo(repository)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def process_repo(repository)
|
49
|
+
process_repo_builds(repository)
|
50
|
+
process_repo_requests(repository)
|
51
|
+
end
|
52
|
+
|
53
|
+
def process_repo_builds(repository) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
54
|
+
threshold = @config.threshold.to_i.months.ago.to_datetime
|
55
|
+
current_build_id = repository.current_build_id || -1
|
56
|
+
repository.builds.where('created_at < ? and id != ?', threshold, current_build_id)
|
57
|
+
.in_batches(of: @config.limit.to_i).map do |builds_batch|
|
58
|
+
if @config.if_backup
|
59
|
+
file_prefix = "repository_#{repository.id}"
|
60
|
+
save_and_destroy_builds_batch(builds_batch, file_prefix)
|
61
|
+
else
|
62
|
+
destroy_builds_batch(builds_batch)
|
63
|
+
end
|
64
|
+
end.compact.reduce(&:&)
|
65
|
+
end
|
66
|
+
|
67
|
+
def process_repo_requests(repository)
|
68
|
+
threshold = @config.threshold.to_i.months.ago.to_datetime
|
69
|
+
repository.requests.where('created_at < ?', threshold)
|
70
|
+
.in_batches(of: @config.limit.to_i).map do |requests_batch|
|
71
|
+
@config.if_backup ? save_and_destroy_requests_batch(requests_batch, repository) : destroy_requests_batch(requests_batch)
|
72
|
+
end.compact
|
73
|
+
end
|
74
|
+
|
75
|
+
private
|
76
|
+
|
77
|
+
def save_and_destroy_builds_batch(builds_batch, file_prefix)
|
78
|
+
builds_export = builds_batch.map(&:attributes)
|
79
|
+
|
80
|
+
dependencies_saved = builds_batch.map do |build|
|
81
|
+
save_build_jobs_and_logs(build, file_prefix)
|
82
|
+
end.reduce(&:&)
|
83
|
+
|
84
|
+
if dependencies_saved
|
85
|
+
file_name = "#{file_prefix}_builds_#{builds_batch.first.id}-#{builds_batch.last.id}.json"
|
86
|
+
pretty_json = JSON.pretty_generate(builds_export)
|
87
|
+
save_file(file_name, pretty_json) ? destroy_builds_batch(builds_batch) : false
|
88
|
+
else
|
89
|
+
false
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def save_build_jobs_and_logs(build, file_prefix)
|
94
|
+
build.jobs.in_batches(of: @config.limit.to_i).map do |jobs_batch|
|
95
|
+
file_prefix = "#{file_prefix}_build_#{build.id}"
|
96
|
+
save_jobs_batch(jobs_batch, file_prefix)
|
97
|
+
end.compact.reduce(&:&)
|
98
|
+
end
|
99
|
+
|
100
|
+
def save_jobs_batch(jobs_batch, file_prefix)
|
101
|
+
jobs_export = jobs_batch.map(&:attributes)
|
102
|
+
|
103
|
+
logs_saved = jobs_batch.map do |job|
|
104
|
+
save_job_logs(job, file_prefix)
|
105
|
+
end.reduce(&:&)
|
106
|
+
|
107
|
+
if logs_saved
|
108
|
+
file_name = "#{file_prefix}_jobs_#{jobs_batch.first.id}-#{jobs_batch.last.id}.json"
|
109
|
+
pretty_json = JSON.pretty_generate(jobs_export)
|
110
|
+
save_file(file_name, pretty_json)
|
111
|
+
else
|
112
|
+
false
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def save_job_logs(job, file_prefix)
|
117
|
+
job.logs.in_batches(of: @config.limit.to_i).map do |logs_batch|
|
118
|
+
file_prefix = "#{file_prefix}_job_#{job.id}"
|
119
|
+
save_logs_batch(logs_batch, file_prefix)
|
120
|
+
end.compact.reduce(&:&)
|
121
|
+
end
|
122
|
+
|
123
|
+
def save_logs_batch(logs_batch, file_prefix)
|
124
|
+
logs_export = logs_batch.map(&:attributes)
|
125
|
+
file_name = "#{file_prefix}_logs_#{logs_batch.first.id}-#{logs_batch.last.id}.json"
|
126
|
+
pretty_json = JSON.pretty_generate(logs_export)
|
127
|
+
save_file(file_name, pretty_json)
|
128
|
+
end
|
129
|
+
|
130
|
+
def destroy_builds_batch(builds_batch)
|
131
|
+
return destroy_builds_batch_dry(builds_batch) if @config.dry_run
|
132
|
+
|
133
|
+
builds_batch.each(&:destroy)
|
134
|
+
end
|
135
|
+
|
136
|
+
def destroy_builds_batch_dry(builds_batch)
|
137
|
+
@dry_run_reporter.add_to_report(:builds, *builds_batch.map(&:id))
|
138
|
+
|
139
|
+
jobs_ids = builds_batch.map do |build|
|
140
|
+
build.jobs.map(&:id) || []
|
141
|
+
end.flatten
|
142
|
+
|
143
|
+
@dry_run_reporter.add_to_report(:jobs, *jobs_ids)
|
144
|
+
|
145
|
+
logs_ids = builds_batch.map do |build|
|
146
|
+
build.jobs.map do |job|
|
147
|
+
job.logs.map(&:id) || []
|
148
|
+
end.flatten || []
|
149
|
+
end.flatten
|
150
|
+
|
151
|
+
@dry_run_reporter.add_to_report(:logs, *logs_ids)
|
152
|
+
end
|
153
|
+
|
154
|
+
def save_and_destroy_requests_batch(requests_batch, repository)
|
155
|
+
requests_export = export_requests(requests_batch)
|
156
|
+
file_name = "repository_#{repository.id}_requests_#{requests_batch.first.id}-#{requests_batch.last.id}.json"
|
157
|
+
pretty_json = JSON.pretty_generate(requests_export)
|
158
|
+
if save_file(file_name, pretty_json)
|
159
|
+
destroy_requests_batch(requests_batch)
|
160
|
+
end
|
161
|
+
requests_export
|
162
|
+
end
|
163
|
+
|
164
|
+
def destroy_requests_batch(requests_batch)
|
165
|
+
return destroy_requests_batch_dry(requests_batch) if @config.dry_run
|
166
|
+
|
167
|
+
requests_batch.each(&:destroy)
|
168
|
+
end
|
169
|
+
|
170
|
+
def destroy_requests_batch_dry(requests_batch)
|
171
|
+
@dry_run_reporter.add_to_report(:requests, *requests_batch.map(&:id))
|
172
|
+
end
|
173
|
+
|
174
|
+
def save_file(file_name, content) # rubocop:disable Metrics/MethodLength
|
175
|
+
return true if @config.dry_run
|
176
|
+
|
177
|
+
saved = false
|
178
|
+
begin
|
179
|
+
unless File.directory?(@config.files_location)
|
180
|
+
FileUtils.mkdir_p(@config.files_location)
|
181
|
+
end
|
182
|
+
|
183
|
+
File.open(file_path(file_name), 'w') do |file|
|
184
|
+
file.write(content)
|
185
|
+
file.close
|
186
|
+
saved = true
|
187
|
+
end
|
188
|
+
rescue => e
|
189
|
+
print "Failed to save #{file_name}, error: #{e.inspect}\n"
|
190
|
+
end
|
191
|
+
saved
|
192
|
+
end
|
193
|
+
|
194
|
+
def file_path(file_name)
|
195
|
+
"#{@config.files_location}/#{file_name}"
|
196
|
+
end
|
197
|
+
|
198
|
+
def export_requests(requests)
|
199
|
+
requests.map do |request|
|
200
|
+
request.attributes
|
201
|
+
end
|
202
|
+
end
|
203
|
+
end
|
204
|
+
end
|
@@ -0,0 +1,146 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Backup
|
4
|
+
class RemoveOrphans
|
5
|
+
attr_reader :config
|
6
|
+
|
7
|
+
def initialize(config, dry_run_reporter=nil)
|
8
|
+
@config = config
|
9
|
+
@dry_run_reporter = dry_run_reporter
|
10
|
+
end
|
11
|
+
|
12
|
+
def dry_run_report
|
13
|
+
@dry_run_reporter.report
|
14
|
+
end
|
15
|
+
|
16
|
+
def run
|
17
|
+
cases.each do |model_block|
|
18
|
+
model_block[:relations].each do |relation|
|
19
|
+
process_table(
|
20
|
+
main_model: model_block[:main_model],
|
21
|
+
related_model: relation[:related_model],
|
22
|
+
fk_name: relation[:fk_name],
|
23
|
+
method: model_block[:method],
|
24
|
+
dry_run_complement: model_block[:dry_run_complement]
|
25
|
+
)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def cases
|
31
|
+
[
|
32
|
+
{
|
33
|
+
main_model: Repository,
|
34
|
+
relations: [
|
35
|
+
{related_model: Build, fk_name: 'current_build_id'},
|
36
|
+
{related_model: Build, fk_name: 'last_build_id'}
|
37
|
+
]
|
38
|
+
}, {
|
39
|
+
main_model: Build,
|
40
|
+
relations: [
|
41
|
+
{related_model: Repository, fk_name: 'repository_id'},
|
42
|
+
{related_model: Commit, fk_name: 'commit_id'},
|
43
|
+
{related_model: Request, fk_name: 'request_id'},
|
44
|
+
{related_model: PullRequest, fk_name: 'pull_request_id'},
|
45
|
+
{related_model: Branch, fk_name: 'branch_id'},
|
46
|
+
{related_model: Tag, fk_name: 'tag_id'}
|
47
|
+
],
|
48
|
+
method: :destroy_all,
|
49
|
+
dry_run_complement: -> (ids) { add_builds_dependencies_to_dry_run_report(ids) }
|
50
|
+
}, {
|
51
|
+
main_model: Job,
|
52
|
+
relations: [
|
53
|
+
{related_model: Repository, fk_name: 'repository_id'},
|
54
|
+
{related_model: Commit, fk_name: 'commit_id'},
|
55
|
+
{related_model: Stage, fk_name: 'stage_id'},
|
56
|
+
]
|
57
|
+
}, {
|
58
|
+
main_model: Branch,
|
59
|
+
relations: [
|
60
|
+
{related_model: Repository, fk_name: 'repository_id'},
|
61
|
+
{related_model: Build, fk_name: 'last_build_id'}
|
62
|
+
]
|
63
|
+
}, {
|
64
|
+
main_model: Tag,
|
65
|
+
relations: [
|
66
|
+
{related_model: Repository, fk_name: 'repository_id'},
|
67
|
+
{related_model: Build, fk_name: 'last_build_id'}
|
68
|
+
]
|
69
|
+
}, {
|
70
|
+
main_model: Commit,
|
71
|
+
relations: [
|
72
|
+
{related_model: Repository, fk_name: 'repository_id'},
|
73
|
+
{related_model: Branch, fk_name: 'branch_id'},
|
74
|
+
{related_model: Tag, fk_name: 'tag_id'}
|
75
|
+
]
|
76
|
+
}, {
|
77
|
+
main_model: Cron,
|
78
|
+
relations: [
|
79
|
+
{related_model: Branch, fk_name: 'branch_id'}
|
80
|
+
]
|
81
|
+
}, {
|
82
|
+
main_model: PullRequest,
|
83
|
+
relations: [
|
84
|
+
{related_model: Repository, fk_name: 'repository_id'}
|
85
|
+
]
|
86
|
+
}, {
|
87
|
+
main_model: SslKey,
|
88
|
+
relations: [
|
89
|
+
{related_model: Repository, fk_name: 'repository_id'}
|
90
|
+
]
|
91
|
+
}, {
|
92
|
+
main_model: Request,
|
93
|
+
relations: [
|
94
|
+
{related_model: Commit, fk_name: 'commit_id'},
|
95
|
+
{related_model: PullRequest, fk_name: 'pull_request_id'},
|
96
|
+
{related_model: Branch, fk_name: 'branch_id'},
|
97
|
+
{related_model: Tag, fk_name: 'tag_id'}
|
98
|
+
]
|
99
|
+
}, {
|
100
|
+
main_model: Stage,
|
101
|
+
relations: [
|
102
|
+
{related_model: Build, fk_name: 'build_id'}
|
103
|
+
]
|
104
|
+
}
|
105
|
+
]
|
106
|
+
end
|
107
|
+
|
108
|
+
def add_builds_dependencies_to_dry_run_report(ids_for_delete)
|
109
|
+
repos_for_delete = Repository.where(current_build_id: ids_for_delete)
|
110
|
+
jobs_for_delete = Job.where(source_id: ids_for_delete)
|
111
|
+
@dry_run_reporter.add_to_report(:repositories, *repos_for_delete.map(&:id))
|
112
|
+
@dry_run_reporter.add_to_report(:jobs, *jobs_for_delete.map(&:id))
|
113
|
+
end
|
114
|
+
|
115
|
+
def process_table(args)
|
116
|
+
main_model = args[:main_model]
|
117
|
+
related_model = args[:related_model]
|
118
|
+
fk_name = args[:fk_name]
|
119
|
+
method = args[:method] || :delete_all
|
120
|
+
dry_run_complement = args[:dry_run_complement]
|
121
|
+
|
122
|
+
main_table = main_model.table_name
|
123
|
+
related_table = related_model.table_name
|
124
|
+
|
125
|
+
for_delete = main_model.find_by_sql(%{
|
126
|
+
select a.*
|
127
|
+
from #{main_table} a
|
128
|
+
left join #{related_table} b
|
129
|
+
on a.#{fk_name} = b.id
|
130
|
+
where
|
131
|
+
a.#{fk_name} is not null
|
132
|
+
and b.id is null;
|
133
|
+
})
|
134
|
+
|
135
|
+
ids_for_delete = for_delete.map(&:id)
|
136
|
+
|
137
|
+
if config.dry_run
|
138
|
+
key = main_table.to_sym
|
139
|
+
@dry_run_reporter.add_to_report(key, *ids_for_delete)
|
140
|
+
dry_run_complement.call(ids_for_delete) if dry_run_complement
|
141
|
+
else
|
142
|
+
main_model.where(id: ids_for_delete).send(method)
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
data/lib/config.rb
CHANGED
@@ -67,19 +67,19 @@ class Config
|
|
67
67
|
@user_id = first_not_nil(
|
68
68
|
args[:user_id],
|
69
69
|
argv_opts[:user_id],
|
70
|
-
ENV['
|
70
|
+
ENV['BACKUP_USER_ID'],
|
71
71
|
config.dig('backup', 'user_id')
|
72
72
|
)
|
73
73
|
@repo_id = first_not_nil(
|
74
74
|
args[:repo_id],
|
75
75
|
argv_opts[:repo_id],
|
76
|
-
ENV['
|
76
|
+
ENV['BACKUP_REPO_ID'],
|
77
77
|
config.dig('backup', 'repo_id')
|
78
78
|
)
|
79
79
|
@org_id = first_not_nil(
|
80
80
|
args[:org_id],
|
81
81
|
argv_opts[:org_id],
|
82
|
-
ENV['
|
82
|
+
ENV['BACKUP_ORG_ID'],
|
83
83
|
config.dig('backup', 'org_id')
|
84
84
|
)
|
85
85
|
@move_logs = first_not_nil(
|
data/lib/db_helper.rb
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class DbHelper
|
4
|
+
def initialize(config)
|
5
|
+
@config = config
|
6
|
+
connect_db
|
7
|
+
end
|
8
|
+
|
9
|
+
def connect_db(config_or_url=@config.database_url)
|
10
|
+
ActiveRecord::Base.establish_connection(config_or_url)
|
11
|
+
end
|
12
|
+
|
13
|
+
def do_in_other_db(config_or_url)
|
14
|
+
saved_config = ActiveRecord::Base.connection_db_config
|
15
|
+
connect_db(config_or_url)
|
16
|
+
result = yield
|
17
|
+
connect_db(saved_config)
|
18
|
+
result
|
19
|
+
end
|
20
|
+
|
21
|
+
def do_without_triggers
|
22
|
+
ActiveRecord::Base.connection.execute('set session_replication_role = replica;')
|
23
|
+
result = yield
|
24
|
+
ActiveRecord::Base.connection.execute('set session_replication_role = default;')
|
25
|
+
result
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class DryRunReporter
|
4
|
+
attr_reader :report
|
5
|
+
|
6
|
+
def initialize
|
7
|
+
@report = {}
|
8
|
+
end
|
9
|
+
|
10
|
+
def add_to_report(key, *values)
|
11
|
+
report[key] = [] if report[key].nil?
|
12
|
+
report[key].concat(values)
|
13
|
+
report[key].uniq!
|
14
|
+
end
|
15
|
+
|
16
|
+
def print_report
|
17
|
+
if @report.to_a.map(&:second).flatten.empty?
|
18
|
+
puts 'Dry run active. No data would be removed in normal run.'
|
19
|
+
else
|
20
|
+
puts 'Dry run active. The following data would be removed in normal run:'
|
21
|
+
|
22
|
+
@report.to_a.map(&:first).each do |symbol|
|
23
|
+
print_report_line(symbol)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def print_report_line(symbol)
|
31
|
+
puts " - #{symbol}: #{@report[symbol].to_json}" if @report[symbol].any?
|
32
|
+
end
|
33
|
+
end
|
data/lib/models/build.rb
CHANGED
@@ -8,6 +8,7 @@ require 'models/repository'
|
|
8
8
|
class Build < Model
|
9
9
|
belongs_to :repository
|
10
10
|
has_many :jobs, -> { order('id') }, foreign_key: :source_id, dependent: :destroy, class_name: 'Job'
|
11
|
+
has_one :repo_for_that_this_build_is_current, foreign_key: :current_build_id, dependent: :destroy, class_name: 'Repository'
|
11
12
|
|
12
13
|
self.table_name = 'builds'
|
13
14
|
end
|
data/lib/models/repository.rb
CHANGED
@@ -6,7 +6,7 @@ require 'models/request'
|
|
6
6
|
|
7
7
|
# Repository model
|
8
8
|
class Repository < Model
|
9
|
-
has_many :builds, -> { order('id') }, foreign_key: :repository_id,
|
9
|
+
has_many :builds, -> { order('id') }, foreign_key: :repository_id, class_name: 'Build'
|
10
10
|
has_many :requests, -> { order('id') }, foreign_key: :repository_id, dependent: :destroy, class_name: 'Request'
|
11
11
|
|
12
12
|
self.table_name = 'repositories'
|
data/lib/travis-backup.rb
CHANGED
@@ -3,6 +3,8 @@
|
|
3
3
|
require 'active_support/core_ext/array'
|
4
4
|
require 'active_support/time'
|
5
5
|
require 'config'
|
6
|
+
require 'db_helper'
|
7
|
+
require 'dry_run_reporter'
|
6
8
|
require 'models/repository'
|
7
9
|
require 'models/log'
|
8
10
|
require 'models/branch'
|
@@ -13,288 +15,36 @@ require 'models/pull_request'
|
|
13
15
|
require 'models/ssl_key'
|
14
16
|
require 'models/request'
|
15
17
|
require 'models/stage'
|
18
|
+
require 'backup/move_logs'
|
19
|
+
require 'backup/remove_orphans'
|
20
|
+
require 'backup/remove_old'
|
16
21
|
|
17
22
|
# main travis-backup class
|
18
23
|
class Backup
|
19
24
|
attr_accessor :config
|
20
|
-
attr_reader :dry_run_report
|
21
25
|
|
22
26
|
def initialize(config_args={})
|
23
27
|
@config = Config.new(config_args)
|
28
|
+
@db_helper = DbHelper.new(@config)
|
24
29
|
|
25
30
|
if @config.dry_run
|
26
|
-
@
|
31
|
+
@dry_run_reporter = DryRunReporter.new
|
27
32
|
end
|
28
|
-
|
29
|
-
connect_db
|
30
33
|
end
|
31
34
|
|
32
|
-
def
|
33
|
-
|
35
|
+
def dry_run_report
|
36
|
+
@dry_run_reporter.report
|
34
37
|
end
|
35
38
|
|
36
39
|
def run(args={})
|
37
|
-
user_id = args[:user_id] || @config.user_id
|
38
|
-
repo_id = args[:repo_id] || @config.repo_id
|
39
|
-
org_id = args[:org_id] || @config.org_id
|
40
|
-
|
41
|
-
if user_id
|
42
|
-
owner_id = user_id
|
43
|
-
owner_type = 'User'
|
44
|
-
elsif org_id
|
45
|
-
owner_id = org_id
|
46
|
-
owner_type = 'Organization'
|
47
|
-
end
|
48
|
-
|
49
40
|
if @config.move_logs
|
50
|
-
|
41
|
+
Backup::MoveLogs.new(@config, @db_helper, @dry_run_reporter).run
|
51
42
|
elsif @config.remove_orphans
|
52
|
-
|
53
|
-
elsif owner_id
|
54
|
-
process_repos_for_owner(owner_id, owner_type)
|
55
|
-
elsif repo_id
|
56
|
-
process_repo_with_id(repo_id)
|
43
|
+
Backup::RemoveOrphans.new(@config, @dry_run_reporter).run
|
57
44
|
else
|
58
|
-
|
59
|
-
end
|
60
|
-
|
61
|
-
print_dry_run_report if @config.dry_run
|
62
|
-
end
|
63
|
-
|
64
|
-
def process_repos_for_owner(owner_id, owner_type)
|
65
|
-
Repository.where('owner_id = ? and owner_type = ?', owner_id, owner_type).order(:id).each do |repository|
|
66
|
-
process_repo(repository)
|
67
|
-
end
|
68
|
-
end
|
69
|
-
|
70
|
-
def process_repo_with_id(repo_id)
|
71
|
-
process_repo(Repository.find(repo_id))
|
72
|
-
end
|
73
|
-
|
74
|
-
def process_all_repos
|
75
|
-
Repository.order(:id).each do |repository|
|
76
|
-
process_repo(repository)
|
77
|
-
end
|
78
|
-
end
|
79
|
-
|
80
|
-
def print_dry_run_report
|
81
|
-
if @dry_run_report.to_a.map(&:second).flatten.empty?
|
82
|
-
puts 'Dry run active. No data would be removed in normal run.'
|
83
|
-
else
|
84
|
-
puts 'Dry run active. The following data would be removed in normal run:'
|
85
|
-
|
86
|
-
@dry_run_report.to_a.map(&:first).each do |symbol|
|
87
|
-
print_dry_run_report_line(symbol)
|
88
|
-
end
|
89
|
-
end
|
90
|
-
end
|
91
|
-
|
92
|
-
def print_dry_run_report_line(symbol)
|
93
|
-
puts " - #{symbol}: #{@dry_run_report[symbol].to_json}" if @dry_run_report[symbol].any?
|
94
|
-
end
|
95
|
-
|
96
|
-
def process_repo(repository)
|
97
|
-
process_repo_builds(repository)
|
98
|
-
process_repo_requests(repository)
|
99
|
-
end
|
100
|
-
|
101
|
-
def process_repo_builds(repository) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
102
|
-
threshold = @config.threshold.to_i.months.ago.to_datetime
|
103
|
-
current_build_id = repository.current_build_id || -1
|
104
|
-
repository.builds.where('created_at < ? and id != ?', threshold, current_build_id)
|
105
|
-
.in_groups_of(@config.limit.to_i, false).map do |builds_batch|
|
106
|
-
@config.if_backup ? save_and_destroy_builds_batch(builds_batch, repository) : destroy_builds_batch(builds_batch)
|
107
|
-
end.compact
|
108
|
-
end
|
109
|
-
|
110
|
-
def process_repo_requests(repository)
|
111
|
-
threshold = @config.threshold.to_i.months.ago.to_datetime
|
112
|
-
repository.requests.where('created_at < ?', threshold)
|
113
|
-
.in_groups_of(@config.limit.to_i, false).map do |requests_batch|
|
114
|
-
@config.if_backup ? save_and_destroy_requests_batch(requests_batch, repository) : destroy_requests_batch(requests_batch)
|
115
|
-
end.compact
|
116
|
-
end
|
117
|
-
|
118
|
-
def move_logs
|
119
|
-
return move_logs_dry if config.dry_run
|
120
|
-
|
121
|
-
connect_db(@config.database_url)
|
122
|
-
Log.order(:id).in_groups_of(@config.limit.to_i, false).map do |logs_batch|
|
123
|
-
log_hashes = logs_batch.as_json
|
124
|
-
connect_db(@config.destination_db_url)
|
125
|
-
|
126
|
-
log_hashes.each do |log_hash|
|
127
|
-
new_log = Log.new(log_hash)
|
128
|
-
new_log.save!
|
129
|
-
end
|
130
|
-
|
131
|
-
connect_db(@config.database_url)
|
132
|
-
|
133
|
-
logs_batch.each(&:destroy)
|
45
|
+
Backup::RemoveOld.new(@config, @dry_run_reporter).run(args)
|
134
46
|
end
|
135
|
-
end
|
136
|
-
|
137
|
-
def move_logs_dry
|
138
|
-
dry_run_report[:logs].concat(Log.order(:id).map(&:id))
|
139
|
-
end
|
140
|
-
|
141
|
-
def remove_orphans
|
142
|
-
remove_orphans_for_table(Repository, 'repositories', 'builds', 'current_build_id')
|
143
|
-
remove_orphans_for_table(Repository, 'repositories', 'builds', 'last_build_id')
|
144
|
-
remove_orphans_for_table(Build, 'builds', 'repositories', 'repository_id')
|
145
|
-
remove_orphans_for_table(Build, 'builds', 'commits', 'commit_id')
|
146
|
-
remove_orphans_for_table(Build, 'builds', 'requests', 'request_id')
|
147
|
-
remove_orphans_for_table(Build, 'builds', 'pull_requests', 'pull_request_id')
|
148
|
-
remove_orphans_for_table(Build, 'builds', 'branches', 'branch_id')
|
149
|
-
remove_orphans_for_table(Build, 'builds', 'tags', 'tag_id')
|
150
|
-
remove_orphans_for_table(Job, 'jobs', 'repositories', 'repository_id')
|
151
|
-
remove_orphans_for_table(Job, 'jobs', 'commits', 'commit_id')
|
152
|
-
remove_orphans_for_table(Job, 'jobs', 'stages', 'stage_id')
|
153
|
-
remove_orphans_for_table(Branch, 'branches', 'repositories', 'repository_id')
|
154
|
-
remove_orphans_for_table(Branch, 'branches', 'builds', 'last_build_id')
|
155
|
-
remove_orphans_for_table(Tag, 'tags', 'repositories', 'repository_id')
|
156
|
-
remove_orphans_for_table(Tag, 'tags', 'builds', 'last_build_id')
|
157
|
-
remove_orphans_for_table(Commit, 'commits', 'repositories', 'repository_id')
|
158
|
-
remove_orphans_for_table(Commit, 'commits', 'branches', 'branch_id')
|
159
|
-
remove_orphans_for_table(Commit, 'commits', 'tags', 'tag_id')
|
160
|
-
remove_orphans_for_table(Cron, 'crons', 'branches', 'branch_id')
|
161
|
-
remove_orphans_for_table(PullRequest, 'pull_requests', 'repositories', 'repository_id')
|
162
|
-
remove_orphans_for_table(SslKey, 'ssl_keys', 'repositories', 'repository_id')
|
163
|
-
remove_orphans_for_table(Request, 'requests', 'commits', 'commit_id')
|
164
|
-
remove_orphans_for_table(Request, 'requests', 'pull_requests', 'pull_request_id')
|
165
|
-
remove_orphans_for_table(Request, 'requests', 'branches', 'branch_id')
|
166
|
-
remove_orphans_for_table(Request, 'requests', 'tags', 'tag_id')
|
167
|
-
remove_orphans_for_table(Stage, 'stages', 'builds', 'build_id')
|
168
|
-
end
|
169
|
-
|
170
|
-
def remove_orphans_for_table(model_class, table_a_name, table_b_name, fk_name)
|
171
|
-
for_delete = model_class.find_by_sql(%{
|
172
|
-
select a.*
|
173
|
-
from #{table_a_name} a
|
174
|
-
left join #{table_b_name} b
|
175
|
-
on a.#{fk_name} = b.id
|
176
|
-
where
|
177
|
-
a.#{fk_name} is not null
|
178
|
-
and b.id is null;
|
179
|
-
})
|
180
47
|
|
181
|
-
if config.dry_run
|
182
|
-
key = table_a_name.to_sym
|
183
|
-
dry_run_report[key] = [] if dry_run_report[key].nil?
|
184
|
-
dry_run_report[key].concat(for_delete.map(&:id))
|
185
|
-
dry_run_report[key].uniq!
|
186
|
-
else
|
187
|
-
model_class.where(id: for_delete.map(&:id)).delete_all
|
188
|
-
end
|
189
|
-
end
|
190
|
-
|
191
|
-
private
|
192
|
-
|
193
|
-
def save_and_destroy_builds_batch(builds_batch, repository)
|
194
|
-
builds_export = export_builds(builds_batch)
|
195
|
-
file_name = "repository_#{repository.id}_builds_#{builds_batch.first.id}-#{builds_batch.last.id}.json"
|
196
|
-
pretty_json = JSON.pretty_generate(builds_export)
|
197
|
-
if save_file(file_name, pretty_json)
|
198
|
-
destroy_builds_batch(builds_batch)
|
199
|
-
end
|
200
|
-
builds_export
|
201
|
-
end
|
202
|
-
|
203
|
-
def destroy_builds_batch(builds_batch)
|
204
|
-
return destroy_builds_batch_dry(builds_batch) if @config.dry_run
|
205
|
-
|
206
|
-
builds_batch.each(&:destroy)
|
207
|
-
end
|
208
|
-
|
209
|
-
def destroy_builds_batch_dry(builds_batch)
|
210
|
-
@dry_run_report[:builds].concat(builds_batch.map(&:id))
|
211
|
-
|
212
|
-
jobs = builds_batch.map do |build|
|
213
|
-
build.jobs.map(&:id) || []
|
214
|
-
end.flatten
|
215
|
-
|
216
|
-
@dry_run_report[:jobs].concat(jobs)
|
217
|
-
|
218
|
-
logs = builds_batch.map do |build|
|
219
|
-
build.jobs.map do |job|
|
220
|
-
job.logs.map(&:id) || []
|
221
|
-
end.flatten || []
|
222
|
-
end.flatten
|
223
|
-
|
224
|
-
@dry_run_report[:logs].concat(logs)
|
225
|
-
end
|
226
|
-
|
227
|
-
def save_and_destroy_requests_batch(requests_batch, repository)
|
228
|
-
requests_export = export_requests(requests_batch)
|
229
|
-
file_name = "repository_#{repository.id}_requests_#{requests_batch.first.id}-#{requests_batch.last.id}.json"
|
230
|
-
pretty_json = JSON.pretty_generate(requests_export)
|
231
|
-
if save_file(file_name, pretty_json)
|
232
|
-
destroy_requests_batch(requests_batch)
|
233
|
-
end
|
234
|
-
requests_export
|
235
|
-
end
|
236
|
-
|
237
|
-
def destroy_requests_batch(requests_batch)
|
238
|
-
return destroy_requests_batch_dry(requests_batch) if @config.dry_run
|
239
|
-
|
240
|
-
requests_batch.each(&:destroy)
|
241
|
-
end
|
242
|
-
|
243
|
-
def destroy_requests_batch_dry(requests_batch)
|
244
|
-
@dry_run_report[:requests].concat(requests_batch.map(&:id))
|
245
|
-
end
|
246
|
-
|
247
|
-
def save_file(file_name, content) # rubocop:disable Metrics/MethodLength
|
248
|
-
return true if @config.dry_run
|
249
|
-
|
250
|
-
saved = false
|
251
|
-
begin
|
252
|
-
unless File.directory?(@config.files_location)
|
253
|
-
FileUtils.mkdir_p(@config.files_location)
|
254
|
-
end
|
255
|
-
|
256
|
-
File.open(file_path(file_name), 'w') do |file|
|
257
|
-
file.write(content)
|
258
|
-
file.close
|
259
|
-
saved = true
|
260
|
-
end
|
261
|
-
rescue => e
|
262
|
-
print "Failed to save #{file_name}, error: #{e.inspect}\n"
|
263
|
-
end
|
264
|
-
saved
|
265
|
-
end
|
266
|
-
|
267
|
-
def file_path(file_name)
|
268
|
-
"#{@config.files_location}/#{file_name}"
|
269
|
-
end
|
270
|
-
|
271
|
-
def export_builds(builds)
|
272
|
-
builds.map do |build|
|
273
|
-
build_export = build.attributes
|
274
|
-
build_export[:jobs] = export_jobs(build.jobs)
|
275
|
-
|
276
|
-
build_export
|
277
|
-
end
|
278
|
-
end
|
279
|
-
|
280
|
-
def export_jobs(jobs)
|
281
|
-
jobs.map do |job|
|
282
|
-
job_export = job.attributes
|
283
|
-
job_export[:logs] = export_logs(job.logs)
|
284
|
-
|
285
|
-
job_export
|
286
|
-
end
|
287
|
-
end
|
288
|
-
|
289
|
-
def export_logs(logs)
|
290
|
-
logs.map do |log|
|
291
|
-
log.attributes
|
292
|
-
end
|
293
|
-
end
|
294
|
-
|
295
|
-
def export_requests(requests)
|
296
|
-
requests.map do |request|
|
297
|
-
request.attributes
|
298
|
-
end
|
48
|
+
@dry_run_reporter.print_report if @config.dry_run
|
299
49
|
end
|
300
50
|
end
|
data/travis-backup.gemspec
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
Gem::Specification.new do |s|
|
2
2
|
s.name = 'travis-backup'
|
3
|
-
s.version = '0.
|
3
|
+
s.version = '0.2.0'
|
4
4
|
s.summary = 'Travis CI backup tool'
|
5
5
|
s.authors = ['Karol Selak']
|
6
6
|
s.required_ruby_version = Gem::Requirement.new(">= 2.3.0")
|
@@ -26,4 +26,5 @@ Gem::Specification.new do |s|
|
|
26
26
|
s.add_development_dependency 'listen'
|
27
27
|
s.add_development_dependency 'rubocop', '~> 0.75.1'
|
28
28
|
s.add_development_dependency 'rubocop-rspec'
|
29
|
+
s.add_development_dependency 'database_cleaner-active_record'
|
29
30
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: travis-backup
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Karol Selak
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-09-
|
11
|
+
date: 2021-09-22 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -192,6 +192,20 @@ dependencies:
|
|
192
192
|
- - ">="
|
193
193
|
- !ruby/object:Gem::Version
|
194
194
|
version: '0'
|
195
|
+
- !ruby/object:Gem::Dependency
|
196
|
+
name: database_cleaner-active_record
|
197
|
+
requirement: !ruby/object:Gem::Requirement
|
198
|
+
requirements:
|
199
|
+
- - ">="
|
200
|
+
- !ruby/object:Gem::Version
|
201
|
+
version: '0'
|
202
|
+
type: :development
|
203
|
+
prerelease: false
|
204
|
+
version_requirements: !ruby/object:Gem::Requirement
|
205
|
+
requirements:
|
206
|
+
- - ">="
|
207
|
+
- !ruby/object:Gem::Version
|
208
|
+
version: '0'
|
195
209
|
description:
|
196
210
|
email:
|
197
211
|
executables:
|
@@ -209,7 +223,6 @@ files:
|
|
209
223
|
- ".gitignore"
|
210
224
|
- ".travis.yml"
|
211
225
|
- Gemfile
|
212
|
-
- Gemfile.lock
|
213
226
|
- README.md
|
214
227
|
- Rakefile
|
215
228
|
- bin/bundle
|
@@ -247,7 +260,12 @@ files:
|
|
247
260
|
- config/spring.rb
|
248
261
|
- db/schema.sql
|
249
262
|
- dump/.keep
|
263
|
+
- lib/backup/move_logs.rb
|
264
|
+
- lib/backup/remove_old.rb
|
265
|
+
- lib/backup/remove_orphans.rb
|
250
266
|
- lib/config.rb
|
267
|
+
- lib/db_helper.rb
|
268
|
+
- lib/dry_run_reporter.rb
|
251
269
|
- lib/models/branch.rb
|
252
270
|
- lib/models/build.rb
|
253
271
|
- lib/models/commit.rb
|
data/Gemfile.lock
DELETED
@@ -1,212 +0,0 @@
|
|
1
|
-
PATH
|
2
|
-
remote: .
|
3
|
-
specs:
|
4
|
-
travis-backup (0.1.0)
|
5
|
-
activerecord
|
6
|
-
bootsnap
|
7
|
-
pg
|
8
|
-
pry
|
9
|
-
rails
|
10
|
-
tzinfo-data
|
11
|
-
|
12
|
-
GEM
|
13
|
-
remote: https://rubygems.org/
|
14
|
-
specs:
|
15
|
-
actioncable (6.1.4.1)
|
16
|
-
actionpack (= 6.1.4.1)
|
17
|
-
activesupport (= 6.1.4.1)
|
18
|
-
nio4r (~> 2.0)
|
19
|
-
websocket-driver (>= 0.6.1)
|
20
|
-
actionmailbox (6.1.4.1)
|
21
|
-
actionpack (= 6.1.4.1)
|
22
|
-
activejob (= 6.1.4.1)
|
23
|
-
activerecord (= 6.1.4.1)
|
24
|
-
activestorage (= 6.1.4.1)
|
25
|
-
activesupport (= 6.1.4.1)
|
26
|
-
mail (>= 2.7.1)
|
27
|
-
actionmailer (6.1.4.1)
|
28
|
-
actionpack (= 6.1.4.1)
|
29
|
-
actionview (= 6.1.4.1)
|
30
|
-
activejob (= 6.1.4.1)
|
31
|
-
activesupport (= 6.1.4.1)
|
32
|
-
mail (~> 2.5, >= 2.5.4)
|
33
|
-
rails-dom-testing (~> 2.0)
|
34
|
-
actionpack (6.1.4.1)
|
35
|
-
actionview (= 6.1.4.1)
|
36
|
-
activesupport (= 6.1.4.1)
|
37
|
-
rack (~> 2.0, >= 2.0.9)
|
38
|
-
rack-test (>= 0.6.3)
|
39
|
-
rails-dom-testing (~> 2.0)
|
40
|
-
rails-html-sanitizer (~> 1.0, >= 1.2.0)
|
41
|
-
actiontext (6.1.4.1)
|
42
|
-
actionpack (= 6.1.4.1)
|
43
|
-
activerecord (= 6.1.4.1)
|
44
|
-
activestorage (= 6.1.4.1)
|
45
|
-
activesupport (= 6.1.4.1)
|
46
|
-
nokogiri (>= 1.8.5)
|
47
|
-
actionview (6.1.4.1)
|
48
|
-
activesupport (= 6.1.4.1)
|
49
|
-
builder (~> 3.1)
|
50
|
-
erubi (~> 1.4)
|
51
|
-
rails-dom-testing (~> 2.0)
|
52
|
-
rails-html-sanitizer (~> 1.1, >= 1.2.0)
|
53
|
-
activejob (6.1.4.1)
|
54
|
-
activesupport (= 6.1.4.1)
|
55
|
-
globalid (>= 0.3.6)
|
56
|
-
activemodel (6.1.4.1)
|
57
|
-
activesupport (= 6.1.4.1)
|
58
|
-
activerecord (6.1.4.1)
|
59
|
-
activemodel (= 6.1.4.1)
|
60
|
-
activesupport (= 6.1.4.1)
|
61
|
-
activestorage (6.1.4.1)
|
62
|
-
actionpack (= 6.1.4.1)
|
63
|
-
activejob (= 6.1.4.1)
|
64
|
-
activerecord (= 6.1.4.1)
|
65
|
-
activesupport (= 6.1.4.1)
|
66
|
-
marcel (~> 1.0.0)
|
67
|
-
mini_mime (>= 1.1.0)
|
68
|
-
activesupport (6.1.4.1)
|
69
|
-
concurrent-ruby (~> 1.0, >= 1.0.2)
|
70
|
-
i18n (>= 1.6, < 2)
|
71
|
-
minitest (>= 5.1)
|
72
|
-
tzinfo (~> 2.0)
|
73
|
-
zeitwerk (~> 2.3)
|
74
|
-
ast (2.4.2)
|
75
|
-
bootsnap (1.7.7)
|
76
|
-
msgpack (~> 1.0)
|
77
|
-
brakeman (5.1.1)
|
78
|
-
builder (3.2.4)
|
79
|
-
byebug (11.1.3)
|
80
|
-
coderay (1.1.3)
|
81
|
-
concurrent-ruby (1.1.9)
|
82
|
-
crass (1.0.6)
|
83
|
-
diff-lcs (1.4.4)
|
84
|
-
erubi (1.10.0)
|
85
|
-
factory_bot (6.2.0)
|
86
|
-
activesupport (>= 5.0.0)
|
87
|
-
ffi (1.15.3)
|
88
|
-
globalid (0.5.2)
|
89
|
-
activesupport (>= 5.0)
|
90
|
-
i18n (1.8.10)
|
91
|
-
concurrent-ruby (~> 1.0)
|
92
|
-
jaro_winkler (1.5.4)
|
93
|
-
listen (3.7.0)
|
94
|
-
rb-fsevent (~> 0.10, >= 0.10.3)
|
95
|
-
rb-inotify (~> 0.9, >= 0.9.10)
|
96
|
-
loofah (2.12.0)
|
97
|
-
crass (~> 1.0.2)
|
98
|
-
nokogiri (>= 1.5.9)
|
99
|
-
mail (2.7.1)
|
100
|
-
mini_mime (>= 0.1.1)
|
101
|
-
marcel (1.0.1)
|
102
|
-
method_source (1.0.0)
|
103
|
-
mini_mime (1.1.1)
|
104
|
-
mini_portile2 (2.6.1)
|
105
|
-
minitest (5.14.4)
|
106
|
-
msgpack (1.4.2)
|
107
|
-
nio4r (2.5.8)
|
108
|
-
nokogiri (1.12.3)
|
109
|
-
mini_portile2 (~> 2.6.1)
|
110
|
-
racc (~> 1.4)
|
111
|
-
parallel (1.20.1)
|
112
|
-
parser (3.0.2.0)
|
113
|
-
ast (~> 2.4.1)
|
114
|
-
pg (1.2.3)
|
115
|
-
pry (0.14.1)
|
116
|
-
coderay (~> 1.1)
|
117
|
-
method_source (~> 1.0)
|
118
|
-
racc (1.5.2)
|
119
|
-
rack (2.2.3)
|
120
|
-
rack-test (1.1.0)
|
121
|
-
rack (>= 1.0, < 3)
|
122
|
-
rails (6.1.4.1)
|
123
|
-
actioncable (= 6.1.4.1)
|
124
|
-
actionmailbox (= 6.1.4.1)
|
125
|
-
actionmailer (= 6.1.4.1)
|
126
|
-
actionpack (= 6.1.4.1)
|
127
|
-
actiontext (= 6.1.4.1)
|
128
|
-
actionview (= 6.1.4.1)
|
129
|
-
activejob (= 6.1.4.1)
|
130
|
-
activemodel (= 6.1.4.1)
|
131
|
-
activerecord (= 6.1.4.1)
|
132
|
-
activestorage (= 6.1.4.1)
|
133
|
-
activesupport (= 6.1.4.1)
|
134
|
-
bundler (>= 1.15.0)
|
135
|
-
railties (= 6.1.4.1)
|
136
|
-
sprockets-rails (>= 2.0.0)
|
137
|
-
rails-dom-testing (2.0.3)
|
138
|
-
activesupport (>= 4.2.0)
|
139
|
-
nokogiri (>= 1.6)
|
140
|
-
rails-html-sanitizer (1.4.1)
|
141
|
-
loofah (~> 2.3)
|
142
|
-
railties (6.1.4.1)
|
143
|
-
actionpack (= 6.1.4.1)
|
144
|
-
activesupport (= 6.1.4.1)
|
145
|
-
method_source
|
146
|
-
rake (>= 0.13)
|
147
|
-
thor (~> 1.0)
|
148
|
-
rainbow (3.0.0)
|
149
|
-
rake (13.0.6)
|
150
|
-
rb-fsevent (0.11.0)
|
151
|
-
rb-inotify (0.10.1)
|
152
|
-
ffi (~> 1.0)
|
153
|
-
rspec-core (3.10.1)
|
154
|
-
rspec-support (~> 3.10.0)
|
155
|
-
rspec-expectations (3.10.1)
|
156
|
-
diff-lcs (>= 1.2.0, < 2.0)
|
157
|
-
rspec-support (~> 3.10.0)
|
158
|
-
rspec-mocks (3.10.2)
|
159
|
-
diff-lcs (>= 1.2.0, < 2.0)
|
160
|
-
rspec-support (~> 3.10.0)
|
161
|
-
rspec-rails (5.0.2)
|
162
|
-
actionpack (>= 5.2)
|
163
|
-
activesupport (>= 5.2)
|
164
|
-
railties (>= 5.2)
|
165
|
-
rspec-core (~> 3.10)
|
166
|
-
rspec-expectations (~> 3.10)
|
167
|
-
rspec-mocks (~> 3.10)
|
168
|
-
rspec-support (~> 3.10)
|
169
|
-
rspec-support (3.10.2)
|
170
|
-
rubocop (0.75.1)
|
171
|
-
jaro_winkler (~> 1.5.1)
|
172
|
-
parallel (~> 1.10)
|
173
|
-
parser (>= 2.6)
|
174
|
-
rainbow (>= 2.2.2, < 4.0)
|
175
|
-
ruby-progressbar (~> 1.7)
|
176
|
-
unicode-display_width (>= 1.4.0, < 1.7)
|
177
|
-
rubocop-rspec (1.41.0)
|
178
|
-
rubocop (>= 0.68.1)
|
179
|
-
ruby-progressbar (1.11.0)
|
180
|
-
sprockets (4.0.2)
|
181
|
-
concurrent-ruby (~> 1.0)
|
182
|
-
rack (> 1, < 3)
|
183
|
-
sprockets-rails (3.2.2)
|
184
|
-
actionpack (>= 4.0)
|
185
|
-
activesupport (>= 4.0)
|
186
|
-
sprockets (>= 3.0.0)
|
187
|
-
thor (1.1.0)
|
188
|
-
tzinfo (2.0.4)
|
189
|
-
concurrent-ruby (~> 1.0)
|
190
|
-
tzinfo-data (1.2021.1)
|
191
|
-
tzinfo (>= 1.0.0)
|
192
|
-
unicode-display_width (1.6.1)
|
193
|
-
websocket-driver (0.7.5)
|
194
|
-
websocket-extensions (>= 0.1.0)
|
195
|
-
websocket-extensions (0.1.5)
|
196
|
-
zeitwerk (2.4.2)
|
197
|
-
|
198
|
-
PLATFORMS
|
199
|
-
ruby
|
200
|
-
|
201
|
-
DEPENDENCIES
|
202
|
-
brakeman
|
203
|
-
byebug
|
204
|
-
factory_bot
|
205
|
-
listen
|
206
|
-
rspec-rails
|
207
|
-
rubocop (~> 0.75.1)
|
208
|
-
rubocop-rspec
|
209
|
-
travis-backup!
|
210
|
-
|
211
|
-
BUNDLED WITH
|
212
|
-
2.1.4
|