-
Notifications
You must be signed in to change notification settings - Fork 47
/
Copy pathgit-sync-check.rb
executable file
·153 lines (131 loc) · 4.26 KB
/
git-sync-check.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
#!/usr/bin/env ruby
# This is executed by `/etc/systemd/system/git-sync-check.service` as User=git
# which is triggered every 10 minutes by `/etc/systemd/system/git-sync-check.timer`.
require 'json'
require 'net/http'
require 'uri'
module Git
# cgit bare repository
GIT_DIR = '/var/git/ruby.git'
# This is retried because ls-remote of GitHub sometimes fails
Error = Class.new(StandardError)
class << self
def show_ref
git('show-ref')
end
def ls_remote(remote)
git('ls-remote', remote)
end
private
def git(*cmd)
out = IO.popen({ 'GIT_DIR' => GIT_DIR }, ['git', *cmd], &:read)
unless $?.success?
raise Git::Error.new("Failed to execute: git #{cmd.join(' ')}")
end
out
end
end
end
module Slack
WEBHOOK_URL = File.read(File.expand_path('~git/config/slack-webhook-alerts')).chomp
NOTIFY_CHANNELS = [
"C5FCXFXDZ", # alerts
"CR2QGFCAE", # alerts-emoji
]
class << self
def notify(message)
attachment = {
title: 'bin/git-sync-check.rb',
title_link: 'https://github.com/ruby/git.ruby-lang.org/blob/master/bin/git-sync-check.rb',
text: message,
color: 'danger',
}
payload = { username: 'ruby/git.ruby-lang.org', attachments: [attachment] }
NOTIFY_CHANNELS.each do |channel|
resp = post(WEBHOOK_URL, payload: payload.merge(channel: channel))
puts "#{resp.code} (#{resp.body}) -- #{payload.to_json} (channel: #{channel})"
end
end
private
def post(url, payload:)
uri = URI.parse(url)
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = (uri.scheme == 'https')
http.start do
req = Net::HTTP::Post.new(uri.path)
req.set_form_data({ payload: payload.to_json })
http.request(req)
end
end
end
end
module GitSyncCheck
class Errors < StandardError
attr_reader :errors
def initialize(errors)
@errors = errors
super('git-sync-check failed')
end
end
def self.check_consistency
# Quickly finish collecting facts to avoid a race condition as much as possible.
ls_remote = Git.ls_remote('github')
show_ref = Git.show_ref
# Start digesting the data after the collection.
remote_refs = Hash[ls_remote.lines.map { |l| rev, ref = l.chomp.split("\t"); [ref, rev] }]
local_refs = Hash[show_ref.lines.map { |l| rev, ref = l.chomp.split(' '); [ref, rev] }]
# Remove refs which are not to be checked here.
refs = remote_refs.keys | local_refs.keys
refs.delete('HEAD') # show-ref does not show it
refs.delete('refs/notes/commits') # it seems too complicated to recover its inconsistency
remote_refs.keys.each { |ref| refs.delete(ref) if ref.match(%r[\Arefs/pull/\d+/\w+\z]) } # pull requests
# Check consistency
errors = {}
refs.each do |ref|
remote_rev = remote_refs[ref]
local_rev = local_refs[ref]
if remote_rev != local_rev
errors[ref] = [remote_rev, local_rev]
end
end
unless errors.empty?
raise Errors.new(errors)
end
end
end
attempts = 3
begin
GitSyncCheck.check_consistency
puts 'SUCCUESS: Everything is consistent.'
rescue GitSyncCheck::Errors => e
attempts -= 1
if attempts > 0
# Automatically fix inconsistency if it's master, but never sync random new branches.
ref = 'refs/heads/master'
if e.errors.key?(ref)
remote_rev, local_rev = e.errors['refs/heads/master']
puts "Fixing inconsistency ref:#{ref.inspect} remote:#{remote_rev.inspect} local:#{local_rev.inspect}"
unless system('/home/git/git.ruby-lang.org/bin/update-ruby.sh', File.basename(ref))
raise "Failed to execute update-ruby.sh for #{ref}"
end
end
sleep 5
retry
end
message = "FAILURE: Following inconsistencies are found.\n"
e.errors.each do |ref, (remote_rev, local_rev)|
message << "ref:#{ref.inspect} remote:#{remote_rev.inspect} local:#{local_rev.inspect}\n"
end
# Slack.notify(message)
puts message
rescue Git::Error => e
attempts -= 1
if attempts > 0
puts "Retrying #{e.class}: #{e.message} (remaining attempts: #{attempts})"
sleep 5
retry
end
Slack.notify("#{e.class}: #{e.message}\n#{e.backtrace.join("\n")}")
rescue => e
Slack.notify("#{e.class}: #{e.message}\n#{e.backtrace.join("\n")}")
end