Skip to content

Commit 8646a6b

Browse files
ci: handle large repo mirroring via GitLab import
Add size-aware mirroring logic that routes large empty projects through GitLab’s server-side import API before pushing, to avoid GitLab.com’s 5 GiB push limit. The workflow now tracks project emptiness, starts authenticated imports for private sources, waits for import completion with clear status/permission/time-out handling, and then proceeds with mirror push. It also removes GitHub-only `refs/pull/*` refs from the local mirror before push.
1 parent a6fca60 commit 8646a6b

1 file changed

Lines changed: 129 additions & 0 deletions

File tree

.github/workflows/__mirror-github-to-gitlab.yml

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ jobs:
5656
const repositoryData = repos.map((repo) => ({
5757
cloneUrl: repo.clone_url,
5858
name: repo.name,
59+
sizeKiB: repo.size,
5960
targetVisibility: repo.visibility === 'public' ? 'public' : 'private',
6061
...gitlabTarget(repo),
6162
}));
@@ -149,6 +150,57 @@ jobs:
149150
curl "${curl_args[@]}" "${url}"
150151
}
151152
153+
wait_for_gitlab_import() {
154+
local project_id="$1"
155+
local source_name="$2"
156+
local deadline="$((SECONDS + 10800))"
157+
local import_error
158+
local import_status
159+
local status
160+
161+
while ((SECONDS < deadline)); do
162+
if ! status="$(gitlab_request GET "${GITLAB_API_URL}/projects/${project_id}/import")"; then
163+
echo "::error title=Mirror failed: ${source_name}::Unable to query the GitLab import."
164+
return 1
165+
fi
166+
if [[ "${status}" != "200" ]]; then
167+
if [[ "${status}" == "403" ]]; then
168+
echo "::error title=Mirror failed: ${source_name}::GITLAB_TOKEN needs Import API Read access."
169+
return 1
170+
fi
171+
echo "::error title=Mirror failed: ${source_name}::Unable to query the GitLab import (HTTP ${status})."
172+
return 1
173+
fi
174+
175+
import_status="$(jq -er '.import_status' "${response_file}")"
176+
case "${import_status}" in
177+
finished)
178+
echo "GitLab server-side import finished."
179+
return 0
180+
;;
181+
scheduled | started)
182+
echo "GitLab server-side import status: ${import_status}."
183+
sleep 30
184+
;;
185+
failed)
186+
import_error="$(jq -r '.import_error // "No import error detail was provided."' "${response_file}")"
187+
import_error="${import_error//$'\r'/ }"
188+
import_error="${import_error//$'\n'/ }"
189+
echo "GitLab import failed: ${import_error}"
190+
echo "::error title=Mirror failed: ${source_name}::GitLab server-side import failed."
191+
return 1
192+
;;
193+
*)
194+
echo "::error title=Mirror failed: ${source_name}::Unexpected GitLab import status: ${import_status}."
195+
return 1
196+
;;
197+
esac
198+
done
199+
200+
echo "::error title=Mirror failed: ${source_name}::GitLab server-side import timed out after three hours."
201+
return 1
202+
}
203+
152204
encoded_group="$(jq -rn --arg value "${GITLAB_GROUP}" '$value | @uri')"
153205
if ! status="$(gitlab_request GET "${GITLAB_API_URL}/groups/${encoded_group}")"; then
154206
echo "::error::Unable to query the GitLab group."
@@ -163,12 +215,17 @@ jobs:
163215
github_auth="$(printf 'x-access-token:%s' "${GITHUB_TOKEN}" | base64 --wrap=0)"
164216
echo "::add-mask::${github_auth}"
165217
218+
# GitLab.com limits each Git push to 5 GiB. Seed large empty projects through GitLab's import API,
219+
# using a lower threshold because GitHub's reported repository size is not the resulting pack size.
220+
gitlab_import_threshold_kib="$((4 * 1024 * 1024))"
221+
166222
repository_count="$(jq -er 'length' "${repository_file}")"
167223
for ((index = 0; index < repository_count; index++)); do
168224
repository="$(jq -ec ".[${index}]" "${repository_file}")"
169225
repository_number="$((index + 1))"
170226
source_name="$(jq -er '.name' <<< "${repository}")"
171227
source_clone_url="$(jq -er '.cloneUrl' <<< "${repository}")"
228+
source_size_kib="$(jq -er '.sizeKiB' <<< "${repository}")"
172229
target_name="$(jq -er '.targetName' <<< "${repository}")"
173230
target_path="$(jq -er '.targetPath' <<< "${repository}")"
174231
target_visibility="$(jq -er '.targetVisibility' <<< "${repository}")"
@@ -180,6 +237,8 @@ jobs:
180237
echo "Visibility: ${target_visibility}"
181238
echo "Checking for the GitLab project..."
182239
240+
project_created=false
241+
183242
if ! status="$(gitlab_request GET "${GITLAB_API_URL}/projects/${encoded_project_path}")"; then
184243
echo "::error title=Mirror failed: ${source_name}::Unable to query the GitLab project."
185244
exit 1
@@ -210,6 +269,7 @@ jobs:
210269
echo "::error title=Mirror failed: ${source_name}::${error_message}"
211270
exit 1
212271
fi
272+
project_created=true
213273
elif [[ "${status}" != "200" ]]; then
214274
echo "::error title=Mirror failed: ${source_name}::Unable to query the GitLab project (HTTP ${status})."
215275
exit 1
@@ -218,6 +278,13 @@ jobs:
218278
fi
219279
220280
project_id="$(jq -er '.id' "${response_file}")"
281+
if [[ "${project_created}" == "true" ]]; then
282+
project_empty=true
283+
else
284+
project_empty="$(
285+
jq -r '(.empty_repo == true) or (.default_branch == null)' "${response_file}"
286+
)"
287+
fi
221288
222289
# Make a private source private before changing metadata or pushing any Git data.
223290
if [[ "${target_visibility}" == "private" ]]; then
@@ -267,6 +334,52 @@ jobs:
267334
268335
target_clone_url="$(jq -er '.ssh_url_to_repo' "${response_file}")"
269336
337+
gitlab_import_started=false
338+
if [[ "${project_empty}" == "true" ]] && \
339+
((source_size_kib >= gitlab_import_threshold_kib)); then
340+
echo "Empty large project detected (${source_size_kib} KiB reported by GitHub)."
341+
echo "Scheduling a GitLab server-side Git import to avoid the 5 GiB push limit..."
342+
if [[ "${target_visibility}" == "private" ]]; then
343+
import_payload="$(
344+
jq -nc \
345+
--arg import_url "${source_clone_url}" \
346+
--arg import_url_user "x-access-token" \
347+
--arg import_url_password "${GITHUB_TOKEN}" \
348+
'{
349+
import_url: $import_url,
350+
import_url_user: $import_url_user,
351+
import_url_password: $import_url_password
352+
}'
353+
)"
354+
else
355+
import_payload="$(
356+
jq -nc \
357+
--arg import_url "${source_clone_url}" \
358+
'{import_url: $import_url}'
359+
)"
360+
fi
361+
362+
if ! status="$(
363+
gitlab_request POST "${GITLAB_API_URL}/projects/${project_id}/import/git" "${import_payload}"
364+
)"; then
365+
echo "::error title=Mirror failed: ${source_name}::Unable to schedule the GitLab import."
366+
exit 1
367+
fi
368+
if [[ "${status}" == "403" ]]; then
369+
echo "::error title=Mirror failed: ${source_name}::GITLAB_TOKEN needs Import API Create access."
370+
exit 1
371+
fi
372+
if [[ "${status}" != "201" ]] && [[ "${status}" != "202" ]]; then
373+
echo "::error title=Mirror failed: ${source_name}::Unable to schedule the GitLab import" \
374+
"(HTTP ${status})."
375+
exit 1
376+
fi
377+
gitlab_import_started=true
378+
elif [[ "${project_empty}" != "true" ]] && \
379+
((source_size_kib >= gitlab_import_threshold_kib)); then
380+
echo "Large project is already populated; using an incremental Git push."
381+
fi
382+
270383
mirror_dir="${temp_root}/repository.git"
271384
echo "Cloning ${source_clone_url} as a mirror..."
272385
if ! git \
@@ -275,6 +388,22 @@ jobs:
275388
echo "::error title=Mirror failed: ${source_name}::Unable to clone the GitHub repository."
276389
exit 1
277390
fi
391+
392+
mapfile -t pull_refs < <(
393+
git -C "${mirror_dir}" for-each-ref --format='%(refname)' refs/pull/
394+
)
395+
if ((${#pull_refs[@]} > 0)); then
396+
echo "Removing ${#pull_refs[@]} GitHub-only pull request refs from the mirror..."
397+
printf 'delete %s\n' "${pull_refs[@]}" | git -C "${mirror_dir}" update-ref --stdin
398+
fi
399+
400+
if [[ "${gitlab_import_started}" == "true" ]]; then
401+
echo "Waiting for the GitLab server-side import to finish..."
402+
if ! wait_for_gitlab_import "${project_id}" "${source_name}"; then
403+
exit 1
404+
fi
405+
fi
406+
278407
echo "Pushing the mirror to ${target_clone_url}..."
279408
if ! git \
280409
-C "${mirror_dir}" \

0 commit comments

Comments
 (0)