GitLab Mirror #50
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # 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 |