├── gitlab ├── __init__.py └── gitlab.py ├── .gitignore ├── .editorconfig ├── Dockerfile ├── .github └── workflows │ ├── release.yml │ ├── github-to-gitee.yml │ ├── github-to-bitbucket.yml │ ├── github-to-gitea.yml │ ├── github-org-to-gitee-org.yml │ ├── github-to-coding.yml │ ├── github-to-github.yml │ └── github-to-gitlab.yml ├── unprotect_branches.py ├── LICENSE ├── Makefile ├── action.yml ├── README.md └── mirror.sh /gitlab/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*.{yml,yaml,sh}] 4 | indent_size = 2 5 | indent_style = space 6 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ghcr.io/gigrator/base:0.0.8 2 | LABEL maintainer="K8sCat " 3 | LABEL repository="https://github.com/k8scat/action-mirror-git.git" 4 | COPY mirror.sh /mirror.sh 5 | ENTRYPOINT ["/mirror.sh"] 6 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*.*.*" 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v2 14 | 15 | - name: Release 16 | uses: softprops/action-gh-release@v0.1.14 17 | 18 | - name: Get version 19 | id: get_version 20 | run: echo "::set-output name=version::${GITHUB_REF/refs\/tags\/v/}" 21 | 22 | - name: Build mirror-git image 23 | run: make build version=${{ steps.get_version.outputs.version }} 24 | 25 | - name: Push to DockerHub 26 | run: | 27 | make push-cr \ 28 | version=${{ steps.get_version.outputs.version }} \ 29 | cr_token=${{ secrets.CR_TOKEN }} 30 | 31 | - name: Push to GitHub CR 32 | run: | 33 | make push-ghcr \ 34 | version=${{ steps.get_version.outputs.version }} \ 35 | ghcr_token=${{ secrets.GITHUB_TOKEN }} 36 | -------------------------------------------------------------------------------- /unprotect_branches.py: -------------------------------------------------------------------------------- 1 | from gitlab.gitlab import GitLab 2 | import sys 3 | 4 | 5 | def unprotect_branches(user, token): 6 | g = GitLab(token) 7 | 8 | projects = g.list_all_user_projects(user) 9 | if projects is None: 10 | return 11 | print(f'found projects: {len(projects)}') 12 | 13 | for project in projects: 14 | project_id = project['id'] 15 | project_name = project['name'] 16 | protected_branches = g.list_protected_branches(project_id) 17 | if len(protected_branches) == 0: 18 | print(f'{project_name} has no protected branches') 19 | continue 20 | for branch in protected_branches: 21 | branch_name = branch['name'] 22 | print(f'unprotect branch: {project_name}/{branch_name}') 23 | g.unprotect_repository_branch(project_id, branch_name) 24 | 25 | 26 | if __name__ == '__main__': 27 | if len(sys.argv) != 3: 28 | print('usage: python unprotect_branches.py ') 29 | sys.exit(1) 30 | user = sys.argv[1] 31 | token = sys.argv[2] 32 | unprotect_branches(user, token) 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 K8sCat 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/github-to-gitee.yml: -------------------------------------------------------------------------------- 1 | name: GitHub to Gitee 2 | 3 | on: 4 | workflow_dispatch: 5 | # schedule: 6 | # - cron: "0 2 * * 3" 7 | 8 | jobs: 9 | mirror: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | 14 | - name: Login Source GitHub 15 | run: | 16 | gh --version 17 | echo "${{ secrets.GITHUB_TOKEN }}" | gh auth login --with-token 18 | gh auth status 19 | 20 | - name: List repos 21 | id: list_repos 22 | run: | 23 | repo_list=$(gh repo list -L 1000 --json name -t '{{range $i, $n := .}}{{if (gt $i 0)}},{{end}}{{$n.name}}{{end}}') 24 | echo "::set-output name=repo_list::${repo_list}" 25 | 26 | - name: GitHub to Gitee 27 | uses: k8scat/action-mirror-git@v0.1.3 28 | with: 29 | source_protocol: https 30 | source_host: github.com 31 | source_username: k8scat 32 | source_token: ${{ secrets.GITHUB_TOKEN }} 33 | dest_protocol: https 34 | dest_host: gitee.com 35 | dest_username: k8scat 36 | dest_token: ${{ secrets.DEST_TOKEN_GITEE }} 37 | mirror_repos: ${{ steps.list_repos.outputs.repo_list }} 38 | ignored_repos: ",Gigrator," 39 | dest_create_repo_script: | 40 | curl \ 41 | -H 'Content-Type: application/json;charset=UTF-8' \ 42 | -d "{\"access_token\":\"${INPUT_DEST_TOKEN}\",\"name\":\"${REPO_NAME}\",\"private\":\"true\"}" \ 43 | 'https://gitee.com/api/v5/user/repos' 44 | lark_webhook: ${{ secrets.LARK_WEBHOOK }} 45 | notify_prefix: "GitHub to Gitee" 46 | ignore_error: "true" 47 | -------------------------------------------------------------------------------- /.github/workflows/github-to-bitbucket.yml: -------------------------------------------------------------------------------- 1 | name: GitHub to BitBucket 2 | 3 | on: 4 | workflow_dispatch: 5 | schedule: 6 | - cron: "0 2 * * 3" 7 | 8 | jobs: 9 | mirror: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | 14 | - name: Login Source GitHub 15 | run: | 16 | gh --version 17 | echo "${{ secrets.GITHUB_TOKEN }}" | gh auth login --with-token 18 | gh auth status 19 | 20 | - name: List repos 21 | id: list_repos 22 | run: | 23 | repo_list=$(gh repo list k8scat -L 1000 --json name -t '{{range $i, $n := .}}{{if (gt $i 0)}},{{end}}{{$n.name}}{{end}}') 24 | echo "::set-output name=repo_list::${repo_list}" 25 | 26 | - name: GitHub to BitBucket 27 | uses: k8scat/action-mirror-git@v0.1.3 28 | with: 29 | source_protocol: https 30 | source_host: github.com 31 | source_username: k8scat 32 | source_token: ${{ secrets.GITHUB_TOKEN }} 33 | dest_protocol: https 34 | dest_host: bitbucket.org 35 | dest_username: k8scat 36 | dest_token: ${{ secrets.DEST_TOKEN_BITBUCKET }} 37 | mirror_repos: ${{ steps.list_repos.outputs.repo_list }} 38 | dest_create_repo_script: | 39 | curl \ 40 | -u "${INPUT_DEST_USERNAME}:${INPUT_DEST_TOKEN}" \ 41 | -H 'Content-Type: application/json' \ 42 | -d '{"scm":"git"}' \ 43 | "https://api.bitbucket.org/2.0/repositories/${INPUT_DEST_USERNAME}/${REPO_NAME}" 44 | lark_webhook: ${{ secrets.LARK_WEBHOOK }} 45 | notify_prefix: "GitHub to BitBucket" 46 | ignore_error: "true" 47 | -------------------------------------------------------------------------------- /.github/workflows/github-to-gitea.yml: -------------------------------------------------------------------------------- 1 | name: GitHub to Gitea 2 | 3 | on: 4 | workflow_dispatch: 5 | # schedule: 6 | # - cron: "0 2 * * 3" 7 | 8 | jobs: 9 | mirror: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | 14 | - name: Login Source GitHub 15 | run: | 16 | gh --version 17 | echo "${{ secrets.GITHUB_TOKEN }}" | gh auth login --with-token 18 | gh auth status 19 | 20 | - name: List repos 21 | id: list_repos 22 | run: | 23 | repo_list=$(gh repo list -L 1000 --json name -t '{{range $i, $n := .}}{{if (gt $i 0)}},{{end}}{{$n.name}}{{end}}') 24 | echo "::set-output name=repo_list::${repo_list}" 25 | 26 | - name: GitHub to Gitea 27 | uses: k8scat/action-mirror-git@v0.1.3 28 | with: 29 | source_protocol: https 30 | source_host: github.com 31 | source_username: k8scat 32 | source_token: ${{ secrets.GITHUB_TOKEN }} 33 | dest_protocol: https 34 | dest_host: gitea.com 35 | dest_username: k8scat 36 | dest_token: ${{ secrets.DEST_TOKEN_GITEA }} 37 | mirror_repos: ${{ steps.list_repos.outputs.repo_list }} 38 | ignored_repos: ",Gigrator," 39 | dest_create_repo_script: | 40 | curl \ 41 | -H 'Content-Type: application/json' \ 42 | -H "Authorization: token ${INPUT_DEST_TOKEN}" \ 43 | -d "{\"name\":\"${REPO_NAME}\",\"private\":true}" \ 44 | 'https://gitea.com/api/v1/user/repos' 45 | lark_webhook: ${{ secrets.LARK_WEBHOOK }} 46 | notify_prefix: "GitHub to Gitea" 47 | ignore_error: "true" 48 | -------------------------------------------------------------------------------- /.github/workflows/github-org-to-gitee-org.yml: -------------------------------------------------------------------------------- 1 | name: GitHub Org to Gitee Org 2 | 3 | on: 4 | workflow_dispatch: 5 | schedule: 6 | - cron: "0 2 * * 3" 7 | 8 | jobs: 9 | mirror: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | 14 | - name: Login Source GitHub 15 | run: | 16 | gh --version 17 | echo "${{ secrets.GITHUB_TOKEN }}" | gh auth login --with-token 18 | gh auth status 19 | 20 | - name: List repos 21 | id: list_repos 22 | run: | 23 | repo_list=$(gh repo list huayin-opensource -L 1000 --json name -t '{{range $i, $n := .}}{{if (gt $i 0)}},{{end}}{{$n.name}}{{end}}') 24 | echo "::set-output name=repo_list::${repo_list}" 25 | 26 | - name: GitHub Org to Gitee Org 27 | uses: k8scat/action-mirror-git@v0.1.3 28 | with: 29 | source_protocol: https 30 | source_host: github.com 31 | source_username: huayin-opensource 32 | source_token: ${{ secrets.GITHUB_TOKEN }} 33 | dest_protocol: https 34 | dest_host: gitee.com 35 | dest_username: huayin-opensource 36 | dest_token: ${{ secrets.DEST_TOKEN_GITEE }} 37 | dest_token_username: k8scat 38 | mirror_repos: ${{ steps.list_repos.outputs.repo_list }} 39 | dest_create_repo_script: | 40 | curl \ 41 | -H 'Content-Type: application/json;charset=UTF-8' \ 42 | -d "{\"access_token\":\"${INPUT_DEST_TOKEN}\",\"name\":\"${REPO_NAME}\",\"private\":\"true\"}" \ 43 | "https://gitee.com/api/v5/orgs/${INPUT_DEST_USERNAME}/repos" 44 | lark_webhook: ${{ secrets.LARK_WEBHOOK }} 45 | notify_prefix: "GitHub Org to Gitee Org" 46 | ignore_error: "true" 47 | -------------------------------------------------------------------------------- /.github/workflows/github-to-coding.yml: -------------------------------------------------------------------------------- 1 | # Coding OpenAPI: https://help.coding.net/openapi 2 | 3 | name: GitHub to Coding 4 | 5 | on: 6 | workflow_dispatch: 7 | # schedule: 8 | # - cron: "0 2 * * 3" 9 | 10 | jobs: 11 | mirror: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | 16 | - name: Login Source GitHub 17 | run: | 18 | gh --version 19 | echo "${{ secrets.GITHUB_TOKEN }}" | gh auth login --with-token 20 | gh auth status 21 | 22 | - name: List repos 23 | id: list_repos 24 | run: | 25 | repo_list=$(gh repo list -L 1000 --json name -t '{{range $i, $n := .}}{{if (gt $i 0)}},{{end}}{{$n.name}}{{end}}') 26 | echo "::set-output name=repo_list::${repo_list}" 27 | 28 | - name: GitHub to Coding 29 | uses: k8scat/action-mirror-git@v0.1.3 30 | with: 31 | source_protocol: https 32 | source_host: github.com 33 | source_username: k8scat 34 | source_token: ${{ secrets.GITHUB_TOKEN }} 35 | dest_protocol: https 36 | dest_host: e.coding.com/k8scat 37 | dest_username: ${{ secrets.CODING_PROJECT_NAME }} 38 | dest_token: ${{ secrets.DEST_TOKEN_CODING }} 39 | dest_token_username: k8scat 40 | mirror_repos: ${{ steps.list_repos.outputs.repo_list }} 41 | dest_create_repo_script: | 42 | curl \ 43 | -H 'Content-Type: application/json' \ 44 | -d "{\"Action\":\"CreateGitDepot\",\"ProjectId\":${{ secrets.CODING_PROJECT_ID }},\"DepotName\":\"${REPO_NAME}\"}" \ 45 | https://e.coding.net/open-api 46 | lark_webhook: ${{ secrets.LARK_WEBHOOK }} 47 | notify_prefix: "GitHub to Coding" 48 | ignore_error: "true" 49 | -------------------------------------------------------------------------------- /.github/workflows/github-to-github.yml: -------------------------------------------------------------------------------- 1 | name: GitHub to GitHub 2 | 3 | on: 4 | workflow_dispatch: 5 | schedule: 6 | - cron: "0 2 * * 3" 7 | 8 | jobs: 9 | mirror: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | 14 | - name: Login Source GitHub 15 | run: | 16 | gh --version 17 | echo "${{ secrets.GITHUB_TOKEN }}" | gh auth login --with-token 18 | gh auth status 19 | 20 | - name: List repos 21 | id: list_repos 22 | run: | 23 | repo_list=$(gh repo list k8scat -L 1000 --json name -t '{{range $i, $n := .}}{{if (gt $i 0)}},{{end}}{{$n.name}}{{end}}') 24 | echo "::set-output name=repo_list::${repo_list}" 25 | 26 | - name: GitHub to GitHub 27 | uses: k8scat/action-mirror-git@v0.1.3 28 | with: 29 | source_protocol: https 30 | source_host: github.com 31 | source_username: k8scat 32 | source_token: ${{ secrets.GITHUB_TOKEN }} 33 | dest_protocol: https 34 | dest_host: github.com 35 | dest_username: k8scat-archived 36 | dest_token: ${{ secrets.DEST_TOKEN_GITHUB }} 37 | dest_token_username: gitobor 38 | mirror_repos: ${{ steps.list_repos.outputs.repo_list }} 39 | dest_create_repo_script: | 40 | if ! gh auth status; then 41 | echo "${INPUT_DEST_TOKEN}" | gh auth login --with-token 42 | gh auth status 43 | fi 44 | 45 | found=$(gh repo list ${INPUT_DEST_USERNAME} -L 1000 --json name -t "{{range .}}{{if (eq .name \"${REPO_NAME}\")}}{{.name}}{{end}}{{end}}") 46 | if [[ -n "${found}" ]]; then 47 | echo "Repo already exists: ${REPO_NAME}" 48 | exit 0 49 | fi 50 | gh repo create "${INPUT_DEST_USERNAME}/${REPO_NAME}" --private 51 | lark_webhook: ${{ secrets.LARK_WEBHOOK }} 52 | notify_prefix: "GitHub to GitHub" 53 | ignore_error: "true" 54 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | os = $(shell uname -s) 2 | 3 | version = 0.1.3 4 | image = mirror-git:$(version) 5 | 6 | cr_user = gigrator 7 | cr_token = 8 | cr_image = $(cr_user)/$(image) 9 | 10 | ghcr = ghcr.io 11 | ghcr_user = k8scat 12 | ghcr_token = 13 | ghcr_image = $(ghcr)/$(ghcr_user)/$(image) 14 | 15 | .PHONY: build 16 | build: 17 | docker build -t $(cr_image) . 18 | docker tag $(cr_image) $(ghcr_image) 19 | 20 | .PHONY: login-cr 21 | login-cr: 22 | docker login -u $(cr_user) -p $(cr_token) 23 | 24 | .PHONY: push-cr 25 | push-cr: login-cr 26 | docker push $(cr_image) 27 | 28 | .PHONY: login-ghcr 29 | login-ghcr: 30 | docker login -u $(ghcr_user) -p $(ghcr_token) $(ghcr) 31 | 32 | .PHONY: push-ghcr 33 | push-ghcr: login-ghcr 34 | docker push $(ghcr_image) 35 | 36 | new_version = 37 | .PHONY: upgrade 38 | upgrade: 39 | ifeq ($(new_version),) 40 | @echo "Usage: make upgrade new_version=" 41 | @exit 1 42 | endif 43 | 44 | ifeq ($(os),Darwin) 45 | sed -i "" 's/$(version)/$(new_version)/g' Makefile 46 | sed -i "" 's/$(version)/$(new_version)/g' action.yml 47 | sed -i "" 's/$(version)/$(new_version)/g' README.md 48 | sed -i "" 's/$(version)/$(new_version)/g' .github/workflows/github-to-github.yml 49 | sed -i "" 's/$(version)/$(new_version)/g' .github/workflows/github-to-gitee.yml 50 | sed -i "" 's/$(version)/$(new_version)/g' .github/workflows/github-to-gitlab.yml 51 | sed -i "" 's/$(version)/$(new_version)/g' .github/workflows/github-to-bitbucket.yml 52 | sed -i "" 's/$(version)/$(new_version)/g' .github/workflows/github-org-to-gitee-org.yml 53 | sed -i "" 's/$(version)/$(new_version)/g' .github/workflows/github-to-gitea.yml 54 | sed -i "" 's/$(version)/$(new_version)/g' .github/workflows/github-to-coding.yml 55 | else 56 | sed -i 's/$(version)/$(new_version)/g' Makefile 57 | sed -i 's/$(version)/$(new_version)/g' action.yml 58 | sed -i 's/$(version)/$(new_version)/g' README.md 59 | sed -i 's/$(version)/$(new_version)/g' .github/workflows/github-to-github.yml 60 | sed -i 's/$(version)/$(new_version)/g' .github/workflows/github-to-gitee.yml 61 | sed -i 's/$(version)/$(new_version)/g' .github/workflows/github-to-gitlab.yml 62 | sed -i 's/$(version)/$(new_version)/g' .github/workflows/github-to-bitbucket.yml 63 | sed -i 's/$(version)/$(new_version)/g' .github/workflows/github-org-to-gitee-org.yml 64 | sed -i 's/$(version)/$(new_version)/g' .github/workflows/github-to-gitea.yml 65 | sed -i 's/$(version)/$(new_version)/g' .github/workflows/github-to-coding.yml 66 | endif -------------------------------------------------------------------------------- /.github/workflows/github-to-gitlab.yml: -------------------------------------------------------------------------------- 1 | name: GitHub to GitLab 2 | 3 | on: 4 | workflow_dispatch: 5 | schedule: 6 | - cron: "0 2 * * 3" 7 | 8 | jobs: 9 | mirror: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | 14 | # https://github.com/actions/setup-python 15 | - uses: actions/setup-python@v2 16 | with: 17 | python-version: "3.6.8" 18 | - name: Unprotect branches 19 | run: | 20 | python -V 21 | pip -V 22 | pip install requests 23 | python unprotect_branches.py k8scat ${{ secrets.DEST_TOKEN_GITLAB }} 24 | 25 | - name: Login Source GitHub 26 | run: | 27 | gh --version 28 | echo "${{ secrets.GITHUB_TOKEN }}" | gh auth login --with-token 29 | gh auth status 30 | 31 | - name: List repos 32 | id: list_repos 33 | run: | 34 | repo_list=$(gh repo list -L 1000 --json name -t '{{range $i, $n := .}}{{if (gt $i 0)}},{{end}}{{$n.name}}{{end}}') 35 | echo "::set-output name=repo_list::${repo_list}" 36 | 37 | - name: GitHub to GitLab 38 | uses: k8scat/action-mirror-git@v0.1.3 39 | with: 40 | source_protocol: https 41 | source_host: github.com 42 | source_username: k8scat 43 | source_token: ${{ secrets.GITHUB_TOKEN }} 44 | dest_protocol: https 45 | dest_host: gitlab.com 46 | dest_username: k8scat 47 | dest_token: ${{ secrets.DEST_TOKEN_GITLAB }} 48 | mirror_repos: ${{ steps.list_repos.outputs.repo_list }} 49 | lark_webhook: ${{ secrets.LARK_WEBHOOK }} 50 | notify_prefix: "GitHub to GitLab" 51 | ignore_error: "true" 52 | dest_create_repo_script: | 53 | # load functions: gitlab_update_project, gitlab_create_project, urlencode 54 | source /mirror-git/functions/url.sh 55 | source /mirror-git/functions/gitlab.sh 56 | 57 | result=$(gitlab_update_project ${INPUT_DEST_TOKEN} $(urlencode ${INPUT_DEST_USERNAME}/${REPO_NAME}) "{\"lfs_enabled\":false}") 58 | if [[ "${result}" = "404" ]]; then 59 | result=$(gitlab_create_project ${INPUT_DEST_TOKEN} "{\"name\":\"${REPO_NAME}\",\"lfs_enabled\":false}") 60 | if [[ "${result}" != "201" ]]; then 61 | notify "Failed to create gitlab project: ${REPO_NAME}" 62 | exit 1 63 | fi 64 | fi 65 | -------------------------------------------------------------------------------- /gitlab/gitlab.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import json 3 | import logging 4 | 5 | 6 | class GitLab: 7 | token = '' 8 | auth_headers = None 9 | base_api = 'https://gitlab.com/api/v4' 10 | client = None 11 | 12 | def __init__(self, token): 13 | self.token = token 14 | self.auth_headers = { 15 | 'PRIVATE-TOKEN': self.token 16 | } 17 | self.client = requests.sessions.Session() 18 | self.client.headers = self.auth_headers 19 | 20 | # https://docs.gitlab.com/ee/api/projects.html#list-user-projects 21 | def list_user_projects(self, user, page=1, per_page=100): 22 | params = { 23 | 'per_page': per_page, 24 | 'page': page, 25 | 'owned': True, 26 | 'order_by': 'created_at', 27 | 'sort': 'desc' 28 | } 29 | with self.client.get(f'{self.base_api}/users/{user}/projects', params=params) as r: 30 | if r.status_code == requests.codes.ok: 31 | projects = json.loads(r.text) 32 | has_next = False 33 | links = r.headers['Link'] 34 | if links: 35 | for link in links.split(', '): 36 | if link.endswith('rel="next"'): 37 | has_next = True 38 | return dict(projects=projects, has_next=has_next) 39 | logging.error(f'list user projects failed: {r.text}') 40 | return None 41 | 42 | def list_all_user_projects(self, user): 43 | projects = [] 44 | page = 1 45 | while True: 46 | res = self.list_user_projects(user, page=page) 47 | if res is None: 48 | return None 49 | 50 | projects.extend(res['projects']) 51 | if not res['has_next']: 52 | return projects 53 | page += 1 54 | 55 | # https://docs.gitlab.com/ee/api/protected_branches.html#list-protected-branches 56 | def list_protected_branches(self, project_id): 57 | with self.client.get(f'{self.base_api}/projects/{project_id}/protected_branches') as r: 58 | if r.status_code == requests.codes.ok: 59 | branches = json.loads(r.text) 60 | return branches 61 | logging.error(f'list protected branches failed: {r.text}') 62 | return None 63 | 64 | # https://docs.gitlab.com/ee/api/protected_branches.html#unprotect-repository-branches 65 | # Unprotects the given protected branch or wildcard protected branch. 66 | def unprotect_repository_branch(self, project_id, branch_name): 67 | with self.client.delete(f'{self.base_api}/projects/{project_id}/protected_branches/{branch_name}') as r: 68 | if r.status_code == requests.codes.no_content: 69 | return 70 | logging.error( 71 | f'unprotect repository branches failed: {r.text}') 72 | -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | # Creating a Docker container action https://docs.github.com/en/actions/creating-actions/creating-a-docker-container-action 2 | # Metadata syntax for GitHub Actions https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions 3 | 4 | name: "Mirror Git" 5 | branding: 6 | icon: "copy" 7 | color: "green" 8 | author: "K8sCat " 9 | description: "Synchronize git repositories like a mirror." 10 | inputs: 11 | source_protocol: 12 | description: "https or ssh" 13 | require: true 14 | default: "https" 15 | source_host: 16 | description: "Git server host without the protocol" 17 | required: true 18 | default: "github.com" 19 | source_port: 20 | description: "Git server port" 21 | required: false 22 | source_username: 23 | description: "Username on source git server" 24 | required: true 25 | source_private_key: 26 | description: "SSH private key" 27 | required: false 28 | source_token: 29 | description: "Personal access token" 30 | required: false 31 | source_token_username: 32 | description: "Username who owned the token" 33 | required: false 34 | dest_protocol: 35 | description: "https or ssh" 36 | require: true 37 | default: "https" 38 | dest_host: 39 | description: "Git server host without the protocol" 40 | required: true 41 | default: "github.com" 42 | dest_port: 43 | description: "Git server port" 44 | required: false 45 | dest_username: 46 | description: "Username on dest git server" 47 | required: true 48 | dest_private_key: 49 | description: "SSH private key" 50 | required: false 51 | dest_token: 52 | description: "Personal access token" 53 | required: false 54 | dest_token_username: 55 | description: "Username who owned the token" 56 | required: false 57 | dest_create_repo_script: 58 | description: "Script used to create repo on dest git server" 59 | required: false 60 | mirror_repos: 61 | description: "Repos to mirror, separated by comma" 62 | required: true 63 | push_tags: 64 | description: "Push tags, true or false" 65 | required: false 66 | default: "true" 67 | skip_tags_repos: 68 | description: "Skip pushing tags for these repos, surrounded by comma" 69 | required: false 70 | ignored_repos: 71 | description: "Ignore these repos, surrounded by comma" 72 | required: false 73 | slack_webhook: 74 | description: "Slack webhook url" 75 | required: false 76 | dingtalk_webhook: 77 | description: "DingTalk webhook url" 78 | required: false 79 | lark_webhook: 80 | description: "Lark webhook url" 81 | required: false 82 | notify_prefix: 83 | description: "Prefix for notify message" 84 | required: false 85 | default: "Mirror Git" 86 | notify_suffix: 87 | description: "Suffix for notify message" 88 | required: false 89 | default: "Powered by https://github.com/k8scat/action-mirror-git" 90 | ignore_error: 91 | description: "Ignore error, true or false" 92 | required: false 93 | default: "false" 94 | runs: 95 | using: "docker" 96 | image: "docker://ghcr.io/k8scat/mirror-git:0.1.3" 97 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # action-mirror-git 2 | 3 | Synchronize git repositories like a mirror. 4 | 5 | ## Examples 6 | 7 | - [GitHub Org to Gitee Org](./.github/workflows/github-org-to-gitee-org.yml) 8 | - [GitHub to Gitee](./.github/workflows/github-to-gitee.yml) 9 | - [GitHub to GitHub](./.github/workflows/github-to-github.yml) 10 | - [GitHub to GitLab](./.github/workflows/github-to-gitlab.yml) 11 | - [GitHub to BitBucket](./.github/workflows/github-to-bitbucket.yml) 12 | - [GitHub to Gitea](./.github/workflows/github-to-gitea.yml) 13 | - [GitHub to Coding](./.github/workflows/github-to-coding.yml) 14 | 15 | ## Support 16 | 17 | - [x] Any git server like GitHub, GitLab, BitBucket, Gitee etc. 18 | - [x] Protocol under HTTPS and SSH 19 | - [x] Sync branches, tags, commits 20 | - [x] Ignore specific repositories 21 | - [x] Specify repositories without pushing tags 22 | - [x] Auto create repository on the dest git server with custom script 23 | - [x] Notify with Slack, DingTalk or Lark 24 | 25 | ### Any git server 26 | 27 | ```yaml 28 | # GitHub 29 | source_host: github.com 30 | 31 | # Self-hosted 32 | source_host: git.example.com 33 | source_port: 8443 34 | ``` 35 | 36 | ### Protocol under HTTPS and SSH 37 | 38 | ```yaml 39 | # HTTPS will use personal access token to authenticate, the token username is required on some git servers like Gitee 40 | source_protocol: https 41 | source_token: github-token-created-under-k8scat-account 42 | 43 | # Gitee 44 | # The source_username will be used as the token username if source_token_username is not specified 45 | source_protocol: https 46 | source_username: huayin-opensource 47 | source_token: gitee-token-created-under-k8scat-account 48 | source_token_username: k8scat 49 | 50 | # SSH requires the private key to authenticate 51 | dest_protocol: ssh 52 | dest_private_key: | 53 | -----BEGIN OPENSSH PRIVATE KEY----- 54 | b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABlwAAAAdzc2gtcn 55 | ... 56 | OOWdOkxLqLsiMAAAAEdGVzdAECAwQFBgc= 57 | -----END OPENSSH PRIVATE KEY----- 58 | ``` 59 | 60 | ### Sync branches, tags, commits 61 | 62 | ```yaml 63 | # Branch and commits will be synced by default 64 | 65 | # Sync tags by default, if you only want to sync branches, you can set push_tags to false 66 | push_tags: "false" 67 | ``` 68 | 69 | Refer to [Duplicating a repository](https://docs.github.com/cn/repositories/creating-and-managing-repositories/duplicating-a-repository). 70 | 71 | ### Ignore specific repositories 72 | 73 | ```yaml 74 | # repo5 and repo6 will not be synced 75 | ignored_repos: "repo5,repo6" 76 | ``` 77 | 78 | ### Specify repositories without pushing tags 79 | 80 | ```yaml 81 | # repo3 and repo4 will only sync branches and commits 82 | skip_tags_repos: "repo3,repo4" 83 | ``` 84 | 85 | ### Auto create repository 86 | 87 | ```yaml 88 | # Custom script to create repository 89 | dest_create_repo_script: | 90 | if ! gh auth status; then 91 | echo "${INPUT_DEST_TOKEN}" | gh auth login --with-token 92 | gh auth status 93 | fi 94 | repo="${INPUT_DEST_USERNAME}/${REPO_NAME}" 95 | found=$(gh repo list ${INPUT_DEST_USERNAME} -L 1000 --json name -t "{{range .}}{{if (eq .name \"${REPO_NAME}\")}}{{.name}}{{end}}{{end}}") 96 | if [[ -n "${found}" ]]; then 97 | echo "repo ${REPO_NAME} already exists" 98 | exit 0 99 | fi 100 | gh repo create "${INPUT_DEST_USERNAME}/${REPO_NAME}" --private 101 | 102 | # Specify the script url to create repository 103 | dest_create_repo_script: https://example.com/create_repo.sh 104 | ``` 105 | 106 | ### Notify 107 | 108 | ```yaml 109 | # Slack 110 | slack_webhook: ${{ secrets.SLACK_WEBHOOK }} 111 | 112 | # DingTalk 113 | dingtalk_webhook: ${{ secrets.DINGTALK_WEBHOOK }} 114 | 115 | # Lark 116 | lark_webhook: ${{ secrets.LARK_WEBHOOK }} 117 | ``` 118 | 119 | ## Run 120 | 121 | ```bash 122 | docker run \ 123 | --rm \ 124 | -e INPUT_SOURCE_PROTOCOL="https" \ 125 | -e INPUT_SOURCE_HOST="github.com" \ 126 | -e INPUT_SOURCE_USERNAME="source_user" \ 127 | -e INPUT_SOURCE_TOKEN="xxx" \ 128 | -e INPUT_DEST_PROTOCOL="ssh" \ 129 | -e INPUT_DEST_HOST="github.com" \ 130 | -e INPUT_DEST_USERNAME="dest_user" \ 131 | -e INPUT_DEST_PRIVATE_KEY="xxx" \ 132 | -e INPUT_PUSH_TAGS="false" \ 133 | -e INPUT_MIRROR_REPOS="repo1,repo2,repo3" \ 134 | -e INPUT_IGNORED_REPOS=",repo1," \ 135 | -e INPUT_SKIP_TAGS_REPOS=",repo2," \ 136 | -e INPUT_DEST_CREATE_REPO_SCRIPT="xxx" \ 137 | -e INPUT_SOURCE_PORT \ 138 | -e INPUT_SOURCE_PRIVATE_KEY \ 139 | -e INPUT_DEST_PORT \ 140 | -e INPUT_DEST_TOKEN \ 141 | -e INPUT_SLACK_WEBHOOK \ 142 | -e INPUT_DINGTALK_WEBHOOK \ 143 | -e INPUT_LARK_WEBHOOK="xxx" \ 144 | -e INPUT_NOTIFY_PREFIX="Mirror Git" \ 145 | -e INPUT_NOTIFY_SUFFIX="Powered by https://github.com/k8scat/action-mirror-git" \ 146 | gigrator/mirror-git:0.1.3 147 | ``` 148 | 149 | ## LICENSE 150 | 151 | [MIT](./LICENSE) 152 | -------------------------------------------------------------------------------- /mirror.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Created by K8sCat 3 | set +e 4 | 5 | PROTOCOL_SSH="ssh" 6 | PROTOCOL_HTTPS="https" 7 | 8 | function init_workdir() { 9 | WORKDIR="/data/repos" 10 | mkdir -p ${WORKDIR} 11 | cd ${WORKDIR} || exit 1 12 | } 13 | 14 | function init_functions() { 15 | for f in /mirror-git/functions/*; do 16 | # shellcheck disable=SC1090 17 | source "${f}" 18 | done 19 | } 20 | 21 | function init_env() { 22 | export INPUT_SOURCE_TOKEN_USERNAME="${INPUT_SOURCE_TOKEN_USERNAME:-${INPUT_SOURCE_USERNAME}}" 23 | export INPUT_SOURCE_PROTOCOL="${INPUT_SOURCE_PROTOCOL:-https}" 24 | export INPUT_SOURCE_HOST="${INPUT_SOURCE_HOST:-github.com}" 25 | export INPUT_DEST_TOKEN_USERNAME="${INPUT_DEST_TOKEN_USERNAME:-${INPUT_DEST_USERNAME}}" 26 | export INPUT_DEST_PROTOCOL="${INPUT_DEST_PROTOCOL:-https}" 27 | export INPUT_DEST_HOST="${INPUT_DEST_HOST:-github.com}" 28 | export INPUT_PUSH_TAGS="${INPUT_PUSH_TAGS:-true}" 29 | export INPUT_NOTIFY_PREFIX="${INPUT_NOTIFY_PREFIX:-Mirror Git}" 30 | export INPUT_NOTIFY_SUFFIX="${INPUT_NOTIFY_SUFFIX:-Powered by https://github.com/k8scat/action-mirror-git}" 31 | export INPUT_IGNORE_ERROR="${INPUT_IGNORE_ERROR:-false}" 32 | } 33 | 34 | function init_git() { 35 | git config --global http.postBuffer 524288000 36 | } 37 | 38 | function init_token_username() { 39 | if [[ -z "${INPUT_SOURCE_TOKEN_USERNAME}" ]]; then 40 | export INPUT_SOURCE_TOKEN_USERNAME="${INPUT_SOURCE_USERNAME}" 41 | fi 42 | if [[ -z "${INPUT_DEST_TOKEN_USERNAME}" ]]; then 43 | export INPUT_DEST_TOKEN_USERNAME="${INPUT_DEST_USERNAME}" 44 | fi 45 | } 46 | 47 | function init_scripts() { 48 | # create_repo 49 | if [[ -n "${INPUT_DEST_CREATE_REPO_SCRIPT}" ]]; then 50 | if [[ $(echo "${INPUT_DEST_CREATE_REPO_SCRIPT}" | wc -l) -eq 1 && "${INPUT_DEST_CREATE_REPO_SCRIPT}" =~ "http".*"://".* ]]; then 51 | if ! curl -L "${INPUT_DEST_CREATE_REPO_SCRIPT}" -o /usr/bin/create_repo; then 52 | notify "Download create_repo script failed" 53 | exit 1 54 | fi 55 | else 56 | echo "${INPUT_DEST_CREATE_REPO_SCRIPT}" > /usr/bin/create_repo 57 | fi 58 | chmod +x /usr/bin/create_repo 59 | fi 60 | } 61 | 62 | # message 63 | function init_message() { 64 | if [[ -n "${INPUT_SLACK_WEBHOOK}" ]]; then 65 | export SLACK_WEBHOOK="${INPUT_SLACK_WEBHOOK}" 66 | fi 67 | if [[ -n "${INPUT_DINGTALK_WEBHOOK}" ]]; then 68 | export DINGTALK_WEBHOOK="${INPUT_DINGTALK_WEBHOOK}" 69 | fi 70 | if [[ -n "${INPUT_LARK_WEBHOOK}" ]]; then 71 | export LARK_WEBHOOK="${INPUT_LARK_WEBHOOK}" 72 | fi 73 | } 74 | 75 | function notify() { 76 | local msg="${1}" 77 | echo "notify: ${msg}" 78 | 79 | if [[ -n "${INPUT_NOTIFY_PREFIX}" ]]; then 80 | msg="[${INPUT_NOTIFY_PREFIX}] ${msg}" 81 | fi 82 | if [[ -n "${GITHUB_SERVER_URL}" && -n "${GITHUB_REPOSITORY}" && -n "${GITHUB_RUN_ID}" ]]; then 83 | local run_url="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" 84 | msg="${msg}\n\nrun_url: ${run_url}" 85 | fi 86 | if [[ -n "${INPUT_NOTIFY_SUFFIX}" ]]; then 87 | msg="${msg}\n\n${INPUT_NOTIFY_SUFFIX}" 88 | fi 89 | 90 | if [[ -n "${SLACK_WEBHOOK}" ]]; then 91 | slack_notify "${msg}" 92 | fi 93 | if [[ -n "${DINGTALK_WEBHOOK}" ]]; then 94 | dingtalk_notify "${msg}" 95 | fi 96 | if [[ -n "${LARK_WEBHOOK}" ]]; then 97 | lark_notify "${msg}" 98 | fi 99 | return 0 100 | } 101 | 102 | function write_ssh_config() { 103 | type=$1 104 | host=$2 105 | port=$3 106 | username=$4 107 | private_key=$5 108 | 109 | local privkey_file="${SSH_DIR}/id_rsa_${username}" 110 | echo "${private_key}" > "${privkey_file}" 111 | chmod 0600 "${privkey_file}" 112 | 113 | new_host="${type}.${username}.${host}" 114 | cat >> "${SSH_DIR}/config" <