Skip to content

Commit 36fd8ab

Browse files
ci: mirror repos to GitLab
1 parent 2472804 commit 36fd8ab

2 files changed

Lines changed: 483 additions & 0 deletions

File tree

Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
1+
---
2+
# Mirror all repositories in the LizardByte GitHub organization to GitLab.
3+
4+
name: Mirror GitHub to GitLab
5+
permissions: {}
6+
7+
on:
8+
schedule:
9+
- cron: '0 3 * * *'
10+
workflow_dispatch:
11+
12+
concurrency:
13+
group: mirror-github-to-gitlab
14+
cancel-in-progress: false
15+
16+
jobs:
17+
mirror:
18+
name: Mirror GitHub to GitLab
19+
permissions: {}
20+
runs-on: ubuntu-latest
21+
steps:
22+
# Keep private repository metadata out of a matrix because matrix values are visible in public workflow runs.
23+
- name: Get repositories
24+
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
25+
with:
26+
github-token: ${{ secrets.GH_BOT_TOKEN }}
27+
script: |
28+
const fs = require('fs');
29+
const opts = github.rest.repos.listForOrg.endpoint.merge({ org: context.repo.owner });
30+
const repos = await github.paginate(opts);
31+
const gitlabTarget = (repo) => {
32+
const prefixNames = { '.': 'dot-', '-': 'dash-', '_': 'underscore-' };
33+
const lowerName = repo.name.toLowerCase();
34+
let targetName = lowerName.replace(
35+
/^[._-]+/,
36+
(prefix) => [...prefix].map((character) => prefixNames[character]).join(''),
37+
);
38+
targetName = targetName
39+
.replace(/[^a-z0-9_.-]+/g, '-')
40+
.replace(/[._-]{2,}/g, '-')
41+
.replace(/[._-]+$/g, '');
42+
43+
if (targetName.endsWith('.git') || targetName.endsWith('.atom')) {
44+
targetName += '-repo';
45+
}
46+
if (!targetName) {
47+
targetName = 'repository';
48+
}
49+
50+
const transformed = targetName !== lowerName;
51+
return {
52+
targetName: transformed ? targetName : repo.name,
53+
targetPath: transformed ? `${targetName}-${repo.id}` : repo.name,
54+
};
55+
};
56+
const repositoryData = repos.map((repo) => ({
57+
cloneUrl: repo.clone_url,
58+
name: repo.name,
59+
targetVisibility: repo.visibility === 'public' ? 'public' : 'private',
60+
...gitlabTarget(repo),
61+
}));
62+
63+
fs.writeFileSync('repositories.json', JSON.stringify(repositoryData), { mode: 0o600 });
64+
core.info(`Prepared ${repositoryData.length} repositories for mirroring.`);
65+
66+
- name: Mirror repositories
67+
shell: bash
68+
env:
69+
GIT_TERMINAL_PROMPT: '0'
70+
GITHUB_TOKEN: ${{ secrets.GH_BOT_TOKEN }}
71+
GITLAB_API_URL: https://gitlab.com/api/v4
72+
GITLAB_GROUP: lizardbyte
73+
GITLAB_TOKEN: ${{ secrets.GITLAB_TOKEN }}
74+
run: |
75+
set -euo pipefail
76+
77+
if [[ -z "${GITHUB_TOKEN}" ]] || [[ -z "${GITLAB_TOKEN}" ]]; then
78+
echo "::error::GH_BOT_TOKEN and GITLAB_TOKEN must both be configured."
79+
exit 1
80+
fi
81+
82+
repository_file="${GITHUB_WORKSPACE}/repositories.json"
83+
response_file="$(mktemp)"
84+
temp_root="$(mktemp -d)"
85+
trap 'rm -f "${response_file}" "${repository_file}"; rm -rf "${temp_root}"' EXIT
86+
87+
gitlab_request() {
88+
local method="$1"
89+
local url="$2"
90+
local data="${3:-}"
91+
local curl_args=(
92+
--silent
93+
--output "${response_file}"
94+
--write-out '%{http_code}'
95+
--request "${method}"
96+
--header "Accept: application/json"
97+
--header "PRIVATE-TOKEN: ${GITLAB_TOKEN}"
98+
)
99+
100+
if [[ -n "${data}" ]]; then
101+
curl_args+=(
102+
--header "Content-Type: application/json"
103+
--data "${data}"
104+
)
105+
fi
106+
107+
curl "${curl_args[@]}" "${url}"
108+
}
109+
110+
encoded_group="$(jq -rn --arg value "${GITLAB_GROUP}" '$value | @uri')"
111+
if ! status="$(gitlab_request GET "${GITLAB_API_URL}/groups/${encoded_group}")"; then
112+
echo "::error::Unable to query the GitLab group."
113+
exit 1
114+
fi
115+
if [[ "${status}" != "200" ]]; then
116+
echo "::error::Unable to query the GitLab group (HTTP ${status})."
117+
exit 1
118+
fi
119+
group_id="$(jq -er '.id' "${response_file}")"
120+
121+
github_auth="$(printf 'x-access-token:%s' "${GITHUB_TOKEN}" | base64 --wrap=0)"
122+
gitlab_auth="$(printf 'oauth2:%s' "${GITLAB_TOKEN}" | base64 --wrap=0)"
123+
echo "::add-mask::${github_auth}"
124+
echo "::add-mask::${gitlab_auth}"
125+
126+
repository_count="$(jq -er 'length' "${repository_file}")"
127+
for ((index = 0; index < repository_count; index++)); do
128+
repository="$(jq -ec ".[${index}]" "${repository_file}")"
129+
source_name="$(jq -er '.name' <<< "${repository}")"
130+
source_clone_url="$(jq -er '.cloneUrl' <<< "${repository}")"
131+
target_name="$(jq -er '.targetName' <<< "${repository}")"
132+
target_path="$(jq -er '.targetPath' <<< "${repository}")"
133+
target_visibility="$(jq -er '.targetVisibility' <<< "${repository}")"
134+
description="Mirror of ${source_clone_url}"
135+
136+
if [[ "${target_visibility}" == "private" ]]; then
137+
echo "::add-mask::${source_name}"
138+
echo "::add-mask::${source_clone_url}"
139+
echo "::add-mask::${target_name}"
140+
echo "::add-mask::${target_path}"
141+
echo "::add-mask::${description}"
142+
fi
143+
144+
echo "Mirroring repository $((index + 1)) of ${repository_count}."
145+
project_path="${GITLAB_GROUP}/${target_path}"
146+
encoded_project_path="$(jq -rn --arg value "${project_path}" '$value | @uri')"
147+
if [[ "${target_visibility}" == "private" ]]; then
148+
echo "::add-mask::${project_path}"
149+
echo "::add-mask::${encoded_project_path}"
150+
fi
151+
152+
if ! status="$(gitlab_request GET "${GITLAB_API_URL}/projects/${encoded_project_path}")"; then
153+
echo "::error::Unable to query the GitLab project for repository $((index + 1))."
154+
exit 1
155+
fi
156+
157+
if [[ "${status}" == "404" ]]; then
158+
payload="$(
159+
jq -nc \
160+
--arg name "${target_name}" \
161+
--arg path "${target_path}" \
162+
--argjson namespace_id "${group_id}" \
163+
--arg visibility "${target_visibility}" \
164+
'{
165+
name: $name,
166+
path: $path,
167+
namespace_id: $namespace_id,
168+
visibility: $visibility,
169+
initialize_with_readme: false
170+
}'
171+
)"
172+
if ! status="$(gitlab_request POST "${GITLAB_API_URL}/projects" "${payload}")"; then
173+
echo "::error::Unable to create the GitLab project for repository $((index + 1))."
174+
exit 1
175+
fi
176+
if [[ "${status}" != "201" ]]; then
177+
echo "::error::Unable to create the GitLab project for repository $((index + 1)) (HTTP ${status})."
178+
exit 1
179+
fi
180+
elif [[ "${status}" != "200" ]]; then
181+
echo "::error::Unable to query the GitLab project for repository $((index + 1)) (HTTP ${status})."
182+
exit 1
183+
fi
184+
185+
project_id="$(jq -er '.id' "${response_file}")"
186+
187+
# Make a private source private before changing metadata or pushing any Git data.
188+
if [[ "${target_visibility}" == "private" ]]; then
189+
privacy_payload="$(jq -nc '{visibility: "private"}')"
190+
if ! status="$(
191+
gitlab_request PUT "${GITLAB_API_URL}/projects/${project_id}" "${privacy_payload}"
192+
)"; then
193+
echo "::error::Unable to secure the GitLab project for repository $((index + 1))."
194+
exit 1
195+
fi
196+
if [[ "${status}" != "200" ]] || \
197+
[[ "$(jq -er '.visibility' "${response_file}")" != "private" ]]; then
198+
echo "::error::GitLab privacy verification failed for repository $((index + 1))."
199+
exit 1
200+
fi
201+
fi
202+
203+
payload="$(
204+
jq -nc \
205+
--arg description "${description}" \
206+
--arg visibility "${target_visibility}" \
207+
'{description: $description, visibility: $visibility}'
208+
)"
209+
if ! status="$(
210+
gitlab_request PUT "${GITLAB_API_URL}/projects/${project_id}" "${payload}"
211+
)"; then
212+
echo "::error::Unable to update the GitLab project for repository $((index + 1))."
213+
exit 1
214+
fi
215+
if [[ "${status}" != "200" ]]; then
216+
echo "::error::Unable to update the GitLab project for repository $((index + 1)) (HTTP ${status})."
217+
exit 1
218+
fi
219+
220+
actual_description="$(jq -er '.description // ""' "${response_file}")"
221+
actual_visibility="$(jq -er '.visibility' "${response_file}")"
222+
if [[ "${actual_description}" != "${description}" ]]; then
223+
echo "::error::GitLab description verification failed for repository $((index + 1))."
224+
exit 1
225+
fi
226+
if [[ "${actual_visibility}" != "${target_visibility}" ]]; then
227+
echo "::error::GitLab visibility verification failed for repository $((index + 1))."
228+
exit 1
229+
fi
230+
231+
target_clone_url="$(jq -er '.http_url_to_repo' "${response_file}")"
232+
if [[ "${target_visibility}" == "private" ]]; then
233+
echo "::add-mask::${target_clone_url}"
234+
fi
235+
236+
mirror_dir="${temp_root}/repository.git"
237+
if ! git \
238+
-c http.https://github.com/.extraheader="AUTHORIZATION: basic ${github_auth}" \
239+
clone --mirror --quiet "${source_clone_url}" "${mirror_dir}"; then
240+
echo "::error::Unable to clone GitHub repository $((index + 1))."
241+
exit 1
242+
fi
243+
if ! git \
244+
-C "${mirror_dir}" \
245+
-c http.https://gitlab.com/.extraheader="AUTHORIZATION: basic ${gitlab_auth}" \
246+
push --mirror --quiet "${target_clone_url}"; then
247+
echo "::error::Unable to push GitLab mirror $((index + 1))."
248+
exit 1
249+
fi
250+
rm -rf "${mirror_dir}"
251+
done
252+
253+
echo "Mirrored ${repository_count} repositories to GitLab."

0 commit comments

Comments
 (0)