Skip to content

GitLab Mirror

GitLab Mirror #45

Workflow file for this run

# Mirrors the GitHub repository to GitLab.com and publishes GitLab releases that
# link back to the canonical GitHub release assets.
name: GitLab Mirror
on:
push:
branches:
- "**"
tags:
- "**"
workflow_run:
workflows:
- Release
types:
- completed
schedule:
- cron: "17 */6 * * *"
workflow_dispatch:
permissions:
contents: read
concurrency:
group: gitlab-mirror
cancel-in-progress: false
env:
GITLAB_MIRROR_REPO: git@gitlab.com:jmrp/TFG-TFM_EPS.git
GITLAB_PROJECT_ID: "85788144"
# Pinned from https://docs.gitlab.com/user/gitlab_com/#ssh-known_hosts-entries
GITLAB_KNOWN_HOSTS: "gitlab.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAfuCHKVTjquxvt6CM6tdG4SLp1Btn/nOeHHE5UOzRdf"
jobs:
repository:
name: Mirror repository
runs-on: ubuntu-latest
if: github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success'
steps:
- name: Configure GitLab SSH key
env:
GITLAB_MIRROR_SSH_KEY: ${{ secrets.GITLAB_MIRROR_SSH_KEY }}
run: |
set -euo pipefail
if [ -z "$GITLAB_MIRROR_SSH_KEY" ]; then
echo "::error::GITLAB_MIRROR_SSH_KEY secret is empty or missing"
exit 1
fi
if [ -z "$GITLAB_KNOWN_HOSTS" ]; then
echo "::error::GITLAB_KNOWN_HOSTS is empty or missing"
exit 1
fi
mkdir -p "$HOME/.ssh"
chmod 700 "$HOME/.ssh"
printf '%s\n' "$GITLAB_MIRROR_SSH_KEY" > "$HOME/.ssh/gitlab_mirror"
chmod 600 "$HOME/.ssh/gitlab_mirror"
printf '%s\n' "$GITLAB_KNOWN_HOSTS" > "$HOME/.ssh/known_hosts"
chmod 644 "$HOME/.ssh/known_hosts"
- name: Push branches and tags to GitLab
env:
GIT_SSH_COMMAND: ssh -i ~/.ssh/gitlab_mirror -o IdentitiesOnly=yes -o StrictHostKeyChecking=yes
run: |
set -euo pipefail
git init --bare mirror.git
git -C mirror.git remote add github "https://github.com/${GITHUB_REPOSITORY}.git"
git -C mirror.git fetch --prune --tags github \
'+refs/heads/*:refs/heads/*' \
'+refs/tags/*:refs/tags/*'
git -C mirror.git remote add gitlab "$GITLAB_MIRROR_REPO"
# --force is required: the GitHub side routinely rewrites main
# (e.g. when a force-push is needed to scrub a leaked bot
# identity, or when a squash commit is amended in place).
# Plain `git push` rejects non-fast-forward updates with
# "rejected - non-fast-forward", which would break the mirror
# silently whenever main is rewritten. The leading `+` on the
# refspec is the equivalent form, but `--force` is explicit
# and easier to grep for. --prune keeps the GitLab side tidy
# by removing refs that have been deleted on GitHub.
git -C mirror.git push --force --prune gitlab \
'+refs/heads/*:refs/heads/*' \
'+refs/tags/*:refs/tags/*'
releases:
name: Sync linked releases
runs-on: ubuntu-latest
if: >-
github.event_name == 'workflow_dispatch' ||
github.event_name == 'schedule' ||
startsWith(github.ref, 'refs/tags/') ||
(github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success')
env:
GH_TOKEN: ${{ github.token }}
GITLAB_API_TOKEN: ${{ secrets.GITLAB_API_TOKEN }}
steps:
- name: Sync GitHub releases to GitLab links
run: |
set -euo pipefail
if [ -z "$GITLAB_API_TOKEN" ]; then
echo "::error::GITLAB_API_TOKEN secret is empty or missing"
exit 1
fi
gitlab_api="https://gitlab.com/api/v4/projects/${GITLAB_PROJECT_ID}"
request_json() {
local method="$1"
local url="$2"
local payload="$3"
curl --fail --show-error --silent \
--request "$method" \
--header "PRIVATE-TOKEN: ${GITLAB_API_TOKEN}" \
--header "Content-Type: application/json" \
--data "$payload" \
"$url" >/dev/null
}
release_exists() {
local tag_encoded="$1"
local status
status=$(curl --silent --show-error --output /dev/null --write-out "%{http_code}" \
--header "PRIVATE-TOKEN: ${GITLAB_API_TOKEN}" \
"${gitlab_api}/releases/${tag_encoded}")
case "$status" in
200) return 0 ;;
404) return 1 ;;
*) echo "Unexpected GitLab release lookup status: ${status}" >&2; exit 1 ;;
esac
}
sync_link() {
local tag_encoded="$1"
local existing_links="$2"
local name="$3"
local url="$4"
local link_type="$5"
local payload
local link_id
payload=$(jq -cn \
--arg name "$name" \
--arg url "$url" \
--arg link_type "$link_type" \
'{name: $name, url: $url, link_type: $link_type}')
link_id=$(jq -r --arg name "$name" '.[] | select(.name == $name) | .id' <<<"$existing_links" | head -n 1)
if [ -n "$link_id" ]; then
request_json PUT "${gitlab_api}/releases/${tag_encoded}/assets/links/${link_id}" "$payload"
else
request_json POST "${gitlab_api}/releases/${tag_encoded}/assets/links" "$payload"
fi
}
delete_stale_links() {
local tag_encoded="$1"
local desired_links="$2"
local current_links
local link
local link_id
local link_name
declare -A desired_names=()
declare -A seen_names=()
while IFS= read -r link_name; do
desired_names["$link_name"]=1
done < <(jq -r '.[].name' <<<"$desired_links")
current_links=$(curl --fail --show-error --silent \
--header "PRIVATE-TOKEN: ${GITLAB_API_TOKEN}" \
"${gitlab_api}/releases/${tag_encoded}/assets/links")
while IFS= read -r link; do
link_id=$(jq -r '.id' <<<"$link")
link_name=$(jq -r '.name' <<<"$link")
if [ -z "${desired_names[$link_name]+x}" ] || [ -n "${seen_names[$link_name]+x}" ]; then
curl --fail --show-error --silent \
--request DELETE \
--header "PRIVATE-TOKEN: ${GITLAB_API_TOKEN}" \
"${gitlab_api}/releases/${tag_encoded}/assets/links/${link_id}" >/dev/null
else
seen_names["$link_name"]=1
fi
done < <(jq -c '.[]' <<<"$current_links")
}
gh api --paginate "repos/${GITHUB_REPOSITORY}/releases" --jq '.[] | select(.draft == false) | .tag_name' |
while IFS= read -r tag; do
[ -n "$tag" ] || continue
release_json=$(gh release view "$tag" --repo "$GITHUB_REPOSITORY" \
--json tagName,name,publishedAt,url,isDraft,isPrerelease,body,assets)
if [ "$(jq -r '.isDraft' <<<"$release_json")" = "true" ]; then
echo "Skipping draft release ${tag}"
continue
fi
tag_encoded=$(jq -nr --arg value "$tag" '$value | @uri')
name=$(jq -r '.name // .tagName' <<<"$release_json")
description=$(jq -r '"This GitLab release mirrors the canonical GitHub release: \(.url)\n\n---\n\n\(.body // "")"' <<<"$release_json")
released_at=$(jq -r '.publishedAt // empty' <<<"$release_json")
create_release_payload=$(jq -cn \
--arg name "$name" \
--arg tag_name "$tag" \
--arg description "$description" \
--arg released_at "$released_at" \
'{name: $name, tag_name: $tag_name, description: $description, released_at: $released_at}')
update_release_payload=$(jq -cn \
--arg name "$name" \
--arg description "$description" \
--arg released_at "$released_at" \
'{name: $name, description: $description, released_at: $released_at}')
if release_exists "$tag_encoded"; then
request_json PUT "${gitlab_api}/releases/${tag_encoded}" "$update_release_payload"
else
request_json POST "${gitlab_api}/releases" "$create_release_payload"
fi
existing_links=$(curl --fail --show-error --silent \
--header "PRIVATE-TOKEN: ${GITLAB_API_TOKEN}" \
"${gitlab_api}/releases/${tag_encoded}/assets/links")
desired_links=$(jq -c '[{name: "GitHub Release", url: .url, link_type: "other"}] + [.assets[]? | {name: .name, url: .url, link_type: "package"}]' <<<"$release_json")
jq -c '.[]' <<<"$desired_links" |
while IFS= read -r link; do
link_name=$(jq -r '.name' <<<"$link")
link_url=$(jq -r '.url' <<<"$link")
link_type=$(jq -r '.link_type' <<<"$link")
[ -n "$link_name" ] && [ -n "$link_url" ] || continue
sync_link "$tag_encoded" "$existing_links" "$link_name" "$link_url" "$link_type"
done
delete_stale_links "$tag_encoded" "$desired_links"
echo "Synced GitLab release links for ${tag}"
done