Skip to content

Commit 35c17b4

Browse files
committed
Cap GitHub tag pagination to prevent unbounded memory growth
`fetch_tags` paged through all tags via GraphQL with no limit. For repos with tens of thousands of tags this accumulated everything into a single array, causing worker.3 to spike to 12GB RSS. Add `max_pages: 10` (1000 tags) matching the existing cap on `fetch_releases` and `load_owner_repos_names`.
1 parent 4806527 commit 35c17b4

File tree

2 files changed

+74
-2
lines changed

2 files changed

+74
-2
lines changed

app/models/hosts/github.rb

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -269,14 +269,16 @@ def load_owner_repos_names(owner, max_pages: 10)
269269
[]
270270
end
271271

272-
def fetch_tags(repository)
272+
def fetch_tags(repository, max_pages: 10)
273273
tags = []
274274
fetch_tags_graphql(repository).tap do |res|
275275
return if res[:data].nil? || res[:data][:repository].nil? || res[:data][:repository][:refs].nil?
276276
tags += map_tags(res)
277-
while res.dig(:data, :repository, :refs, :pageInfo, :hasNextPage)
277+
pages_fetched = 1
278+
while pages_fetched < max_pages && res.dig(:data, :repository, :refs, :pageInfo, :hasNextPage)
278279
res = fetch_tags_graphql(repository, res[:data][:repository][:refs][:pageInfo][:endCursor])
279280
tags += map_tags(res)
281+
pages_fetched += 1
280282
end
281283
end
282284
tags

test/models/hosts/github_test.rb

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,76 @@ class Hosts::GithubTest < ActiveSupport::TestCase
8585
end
8686
end
8787

88+
context 'fetch_tags' do
89+
should 'fetch tags via graphql' do
90+
graphql_response = {
91+
data: {
92+
repository: {
93+
refs: {
94+
pageInfo: { startCursor: 'abc', hasNextPage: false, endCursor: 'def' },
95+
nodes: [
96+
{ name: 'v1.0.0', target: { __typename: 'Commit', oid: 'sha1', committer: { date: '2026-01-01' } } }
97+
]
98+
}
99+
}
100+
}
101+
}
102+
103+
@github.expects(:fetch_tags_graphql).with(@repository).returns(graphql_response)
104+
105+
result = @github.fetch_tags(@repository)
106+
107+
assert_equal 1, result.length
108+
assert_equal 'v1.0.0', result.first[:name]
109+
assert_equal 'sha1', result.first[:sha]
110+
end
111+
112+
should 'stop after max_pages' do
113+
page1_response = {
114+
data: {
115+
repository: {
116+
refs: {
117+
pageInfo: { startCursor: 'a', hasNextPage: true, endCursor: 'cursor1' },
118+
nodes: [
119+
{ name: 'v1.0', target: { __typename: 'Commit', oid: 'sha1', committer: { date: '2026-01-01' } } }
120+
]
121+
}
122+
}
123+
}
124+
}
125+
126+
page2_response = {
127+
data: {
128+
repository: {
129+
refs: {
130+
pageInfo: { startCursor: 'b', hasNextPage: true, endCursor: 'cursor2' },
131+
nodes: [
132+
{ name: 'v2.0', target: { __typename: 'Commit', oid: 'sha2', committer: { date: '2026-01-02' } } }
133+
]
134+
}
135+
}
136+
}
137+
}
138+
139+
@github.expects(:fetch_tags_graphql).with(@repository).returns(page1_response)
140+
@github.expects(:fetch_tags_graphql).with(@repository, 'cursor1').returns(page2_response)
141+
142+
result = @github.fetch_tags(@repository, max_pages: 2)
143+
144+
assert_equal 2, result.length
145+
assert_equal 'v1.0', result.first[:name]
146+
assert_equal 'v2.0', result.last[:name]
147+
end
148+
149+
should 'return nil when graphql returns no data' do
150+
@github.expects(:fetch_tags_graphql).with(@repository).returns({ data: nil })
151+
152+
result = @github.fetch_tags(@repository)
153+
154+
assert_nil result
155+
end
156+
end
157+
88158
context 'load_owner_repos_names' do
89159
setup do
90160
@owner = OpenStruct.new(login: 'testuser')

0 commit comments

Comments
 (0)