71 lines
3.6 KiB
Python
71 lines
3.6 KiB
Python
#!/usr/bin/env python3
|
|
"""Reconcile all business Git references from Gitea to GitHub under one lock."""
|
|
|
|
import argparse
|
|
import os
|
|
from pathlib import Path
|
|
import tempfile
|
|
import time
|
|
|
|
from ci import SOURCE, locked, request_json, require, run, save
|
|
|
|
TARGET = 'https://github.com/eezd/AlphaPulse.git'
|
|
DIRECTORY = Path('/docker/gitea/mirrors/AlphaPulse')
|
|
|
|
|
|
def refs(repository, remote=None, env=None):
|
|
args = ['ls-remote', remote, 'refs/heads/*', 'refs/tags/*'] if remote else [
|
|
'for-each-ref', '--format=%(objectname)\t%(refname)', 'refs/heads/', 'refs/tags/']
|
|
output = run('git', '-C', repository, *args, env=env)
|
|
return {ref: sha for line in output.splitlines() for sha, ref in [line.split()] if not ref.endswith('^{}')}
|
|
|
|
|
|
def synchronize(directory, source, target, env=None):
|
|
directory = Path(directory)
|
|
with locked(directory / '.mirror.lock'):
|
|
repository = directory / 'repository.git'
|
|
if not repository.exists():
|
|
run('git', 'init', '--bare', repository)
|
|
for _ in range(3):
|
|
run('git', '-C', repository, 'fetch', '--prune', '--no-tags', source,
|
|
'+refs/heads/*:refs/heads/*', '+refs/tags/*:refs/tags/*', env=env)
|
|
snapshot = refs(repository)
|
|
require('refs/heads/main' in snapshot, 'Source main is missing; refusing destructive synchronization')
|
|
if run('git', '-C', repository, 'log', '--all', '--format=', '--name-only', '--', '.gitattributes', '**/.gitattributes'):
|
|
run('git', '-C', repository, 'lfs', 'fetch', '--all', source, env=env)
|
|
run('git', '-C', repository, 'lfs', 'push', '--all', target, env=env)
|
|
run('git', '-C', repository, 'push', '--atomic', '--prune', target,
|
|
'+refs/heads/*:refs/heads/*', '+refs/tags/*:refs/tags/*', env=env)
|
|
require(refs(repository, target, env) == snapshot, 'GitHub references differ from the selected source snapshot')
|
|
if refs(repository, source, env) == snapshot:
|
|
save(directory / 'last-sync.json', {'refs': snapshot, 'verified_at': time.time()})
|
|
return
|
|
raise RuntimeError('Source kept changing; retry the mirror')
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.parse_args()
|
|
token = os.environ.get('ALL_GITHUB_TOKEN')
|
|
require(token, 'Missing account-level ALL_GITHUB_TOKEN')
|
|
identity = request_json('https://api.github.com/user', token=token)
|
|
require(identity['login'] == 'eezd', 'GitHub token belongs to an unexpected account')
|
|
repository = request_json('https://api.github.com/repos/eezd/AlphaPulse', token=token)
|
|
require(repository.get('permissions', {}).get('push'), 'GitHub repository write permission is required')
|
|
# The helper contains no credentials; Git reads the token from the task environment.
|
|
with tempfile.TemporaryDirectory(prefix='ap-mirror-auth-') as temporary:
|
|
helper = Path(temporary) / 'askpass'
|
|
helper.write_text('#!/bin/sh\ncase "$1" in\n'
|
|
' *github.com*) case "$1" in *Username*) printf "%s" "x-access-token" ;; '
|
|
'*) printf "%s" "$ALL_GITHUB_TOKEN" ;; esac ;;\n'
|
|
' *) exit 1 ;;\nesac\n')
|
|
helper.chmod(0o700)
|
|
env = {**os.environ, 'GIT_ASKPASS': str(helper), 'GIT_TERMINAL_PROMPT': '0',
|
|
'GIT_CONFIG_COUNT': '1', 'GIT_CONFIG_KEY_0': 'credential.helper', 'GIT_CONFIG_VALUE_0': ''}
|
|
synchronize(DIRECTORY, SOURCE, TARGET, env)
|
|
print('GitHub business references and history verified')
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|