Skip to content

Commit e615165

Browse files
sorahclaude
andcommitted
extract GitHubPusher from GenerateSponsorsYamlFileJob
Separate data generation from GitHub interaction by moving push_to_github internals (branch reset, commit with retry, staleness check, PR upsert) into a nested GitHubPusher PORO. The pusher takes explicit constructor args instead of relying on job instance variables. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 84aec8a commit e615165

File tree

1 file changed

+96
-74
lines changed

1 file changed

+96
-74
lines changed

app/jobs/generate_sponsors_yaml_file_job.rb

Lines changed: 96 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
11
class GenerateSponsorsYamlFileJob < ApplicationJob
2-
delegate :octokit, :base_branch, to: :github_installation
3-
42
def perform(conference, push: true)
53
@conference = conference
64

@@ -126,97 +124,121 @@ def json_data
126124
@json_data = data ? "#{data.to_json}\n" : nil
127125
end
128126

129-
MAX_PUSH_RETRIES = 3
130-
131127
def push_to_github
132128
return unless repo
133-
return if yaml_data.nil? # to generate
129+
return if yaml_data.nil?
130+
134131
push_id = @last_id || "event-#{@last_event_editing_history_id}"
135-
@branch_name = "sponsor-app/#{@conference.slug}"
136-
@filepath = repo.path
137-
pr_title = "Update sponsors.yml for #{@conference.slug} (#{push_id})"
138-
139-
# Check if existing branch already has newer data
140-
if branch_has_newer_data?
141-
Rails.logger.info "GenerateSponsorsYamlFileJob: branch has newer data, skipping"
142-
return
143-
end
132+
GitHubPusher.new(
133+
conference: @conference,
134+
filepath: repo.path,
135+
content: yaml_data,
136+
last_editing_history_id: @last_id,
137+
push_id:,
138+
).push
139+
end
144140

145-
# Delete and recreate branch from base HEAD
146-
begin
147-
octokit.delete_branch(repo.name, @branch_name)
148-
rescue Octokit::UnprocessableEntity
149-
end
141+
# For debugging
142+
def self.get_octokit(repo)
143+
GithubInstallation.new(repo).octokit
144+
end
150145

151-
head = octokit.branch(repo.name, base_branch)
152-
octokit.create_ref(repo.name, "refs/heads/#{@branch_name}", head[:commit][:sha])
146+
class GitHubPusher
147+
MAX_RETRIES = 3
153148

154-
# Get blob_sha from base branch (acts as optimistic lock)
155-
begin
156-
blob_sha = octokit.contents(repo.name, path: @filepath)[:sha]
157-
rescue Octokit::NotFound
158-
blob_sha = nil
149+
delegate :octokit, :base_branch, to: :github_installation
150+
151+
def initialize(conference:, filepath:, content:, last_editing_history_id:, push_id:)
152+
@conference = conference
153+
@repo = conference.github_repo
154+
@filepath = filepath
155+
@content = content
156+
@last_id = last_editing_history_id
157+
@branch_name = "sponsor-app/#{conference.slug}"
158+
@pr_title = "Update sponsors.yml for #{conference.slug} (#{push_id})"
159159
end
160160

161-
push_retries = 0
162-
begin
163-
octokit.update_contents(repo.name, @filepath, pr_title, blob_sha, yaml_data, branch: @branch_name)
164-
rescue Octokit::Conflict, Octokit::UnprocessableEntity => e
165-
# Conflict: another job committed first. Check if our data is newer.
166-
if push_retries < MAX_PUSH_RETRIES && !branch_has_newer_data?
167-
push_retries += 1
168-
# Re-read blob_sha from the branch (now updated by the other job)
169-
begin
170-
blob_sha = octokit.contents(repo.name, path: @filepath, ref: @branch_name)[:sha]
171-
rescue Octokit::NotFound
172-
blob_sha = nil
173-
end
174-
Rails.logger.info "GenerateSponsorsYamlFileJob: commit conflict, retrying (#{push_retries}/#{MAX_PUSH_RETRIES})"
175-
retry
176-
else
177-
Rails.logger.info "GenerateSponsorsYamlFileJob: commit conflict and branch has newer data (or max retries), skipping"
161+
def push
162+
return unless @repo
163+
164+
if branch_has_newer_data?
165+
Rails.logger.info "GenerateSponsorsYamlFileJob: branch has newer data, skipping"
178166
return
179167
end
180-
end
181168

182-
create_or_update_pull_request(pr_title)
183-
end
169+
reset_branch
170+
commit_content
171+
create_or_update_pull_request
172+
end
184173

185-
# For debugging
186-
def self.get_octokit(repo)
187-
GithubInstallation.new(repo).octokit
188-
end
174+
private
189175

190-
private
176+
def reset_branch
177+
begin
178+
octokit.delete_branch(@repo.name, @branch_name)
179+
rescue Octokit::UnprocessableEntity
180+
end
191181

192-
def branch_has_newer_data?
193-
existing = octokit.contents(repo.name, path: @filepath, ref: @branch_name)
194-
existing_content = Base64.decode64(existing[:content])
195-
if existing_content =~ /last_editing_history: (\d+)/
196-
return @last_id && $1.to_i > @last_id
182+
head = octokit.branch(@repo.name, base_branch)
183+
octokit.create_ref(@repo.name, "refs/heads/#{@branch_name}", head[:commit][:sha])
197184
end
198-
false
199-
rescue Octokit::NotFound
200-
false # Branch or file doesn't exist
201-
end
202185

203-
def create_or_update_pull_request(pr_title)
204-
owner = repo.name.split('/')[0]
205-
existing_prs = octokit.pull_requests(repo.name, state: 'open', head: "#{owner}:#{@branch_name}")
206-
if existing_prs.any?
207-
octokit.update_pull_request(repo.name, existing_prs[0][:number], title: pr_title)
208-
else
186+
def commit_content
209187
begin
210-
octokit.create_pull_request(repo.name, base_branch, @branch_name, pr_title, nil)
211-
rescue Octokit::UnprocessableEntity
212-
# Concurrent job already created PR
213-
existing_prs = octokit.pull_requests(repo.name, state: 'open', head: "#{owner}:#{@branch_name}")
214-
octokit.update_pull_request(repo.name, existing_prs[0][:number], title: pr_title) if existing_prs.any?
188+
blob_sha = octokit.contents(@repo.name, path: @filepath)[:sha]
189+
rescue Octokit::NotFound
190+
blob_sha = nil
191+
end
192+
193+
retries = 0
194+
begin
195+
octokit.update_contents(@repo.name, @filepath, @pr_title, blob_sha, @content, branch: @branch_name)
196+
rescue Octokit::Conflict, Octokit::UnprocessableEntity => e
197+
if retries < MAX_RETRIES && !branch_has_newer_data?
198+
retries += 1
199+
begin
200+
blob_sha = octokit.contents(@repo.name, path: @filepath, ref: @branch_name)[:sha]
201+
rescue Octokit::NotFound
202+
blob_sha = nil
203+
end
204+
Rails.logger.info "GenerateSponsorsYamlFileJob: commit conflict, retrying (#{retries}/#{MAX_RETRIES})"
205+
retry
206+
else
207+
Rails.logger.info "GenerateSponsorsYamlFileJob: commit conflict and branch has newer data (or max retries), skipping"
208+
return
209+
end
215210
end
216211
end
217-
end
218212

219-
def github_installation
220-
@github_installation ||= GithubInstallation.new(repo.name, branch: repo.branch)
213+
def branch_has_newer_data?
214+
existing = octokit.contents(@repo.name, path: @filepath, ref: @branch_name)
215+
existing_content = Base64.decode64(existing[:content])
216+
if existing_content =~ /last_editing_history: (\d+)/
217+
return @last_id && $1.to_i > @last_id
218+
end
219+
false
220+
rescue Octokit::NotFound
221+
false # Branch or file doesn't exist
222+
end
223+
224+
def create_or_update_pull_request
225+
owner = @repo.name.split('/')[0]
226+
existing_prs = octokit.pull_requests(@repo.name, state: 'open', head: "#{owner}:#{@branch_name}")
227+
if existing_prs.any?
228+
octokit.update_pull_request(@repo.name, existing_prs[0][:number], title: @pr_title)
229+
else
230+
begin
231+
octokit.create_pull_request(@repo.name, base_branch, @branch_name, @pr_title, nil)
232+
rescue Octokit::UnprocessableEntity
233+
# Concurrent job already created PR
234+
existing_prs = octokit.pull_requests(@repo.name, state: 'open', head: "#{owner}:#{@branch_name}")
235+
octokit.update_pull_request(@repo.name, existing_prs[0][:number], title: @pr_title) if existing_prs.any?
236+
end
237+
end
238+
end
239+
240+
def github_installation
241+
@github_installation ||= GithubInstallation.new(@repo.name, branch: @repo.branch)
242+
end
221243
end
222244
end

0 commit comments

Comments
 (0)