Diffinery

codician/diffinerypublic

Open-source, agent-first GitHub and GitHub Actions alternative that runs on your own Cloudflare account: git hosting, pull requests, code review and drop-in GitHub Actions YAML on Workers, Durable Objects, R2 and Containers.

Mirror script: hi@codician.com identity, tags and releases (#307)

* Mirror script: GitHub commits use codician-team <hi@codician.com>
* Mirror script: mirror the tags on main as well
* Mirror script: mirror releases on the mirrored tags
test-bot authored 1 parent 25a497ecommit 629b6aba6d52f56ed5cb049c8a8802e851efaea1Browse files
1 file changed+64−5
modifiedscripts/mirror-to-github.sh+64−5View file
@@ -6,7 +6,8 @@
66 # personal contributors.
77 # The rewrite is deterministic: the same Diffinery history always gives the same
88 # GitHub commits, so a sync is a fast-forward unless Diffinery's own history was
9-# rewritten (then GitHub follows it). Only main is mirrored.
9+# rewritten (then GitHub follows it). Only main, the tags on it and their releases
10+# are mirrored.
1011 #
1112 # Reads Diffinery with DIFFINERY_TOKEN when set (not needed once the repo is public);
1213 # MIRROR_SRC can also be a local clone that has main. Pushes with GITHUB_MIRROR_TOKEN
@@ -22,7 +23,7 @@
2223 SRC=${MIRROR_SRC:-https://diffinery.com/codician/diffinery.git}
2324 DST=${MIRROR_DST:-https://github.com/codician-team/diffinery.git}
2425 NAME=${MIRROR_NAME:-codician-team}
25-EMAIL=${MIRROR_EMAIL:-codician-team@users.noreply.diffinery.com}
26+EMAIL=${MIRROR_EMAIL:-hi@codician.com}
2627 dry_run=${1:-}
2728
2829 work=${MIRROR_WORKDIR:-}
@@ -62,7 +63,65 @@
6263 fi
6364 remote=$(git "${auth[@]}" ls-remote "$DST" refs/heads/main | cut -f1)
6465 if [ "$remote" = "$dst_head" ]; then
65- echo "GitHub is up to date."
66- exit 0
66+ echo "GitHub main is up to date."
67+else
68+ git "${auth[@]}" push --force "$DST" main:main
6769 fi
68-git "${auth[@]}" push --force "$DST" main:main
70+
71+# Tags on main follow their commits, as lightweight tags (so no tagger identity).
72+declare -A rewritten
73+while read -r old new; do rewritten[$old]=$new; done < <(tail -n +2 filter-repo/commit-map)
74+remote_tags=$(git "${auth[@]}" ls-remote --tags "$DST")
75+mirrored=()
76+for tag in $(git tag); do
77+ target=${rewritten[$(git rev-parse "$tag^{commit}")]:-}
78+ [ -n "$target" ] && [ "$target" != 0000000000000000000000000000000000000000 ] || continue
79+ if grep -qx "$target"$'\t'"refs/tags/$tag" <<<"$remote_tags" || git "${auth[@]}" push --force "$DST" "$target:refs/tags/$tag"; then
80+ mirrored+=("$tag")
81+ else
82+ echo "warning: could not push tag $tag" >&2
83+ fi
84+done
85+
86+# Releases on the mirrored tags get the same name, notes and flags on GitHub (assets aren't copied).
87+# Needs GITHUB_MIRROR_TOKEN for GitHub's API.
88+if [ -n "${GITHUB_MIRROR_TOKEN:-}" ] && [ ${#mirrored[@]} -gt 0 ]; then
89+ MIRROR_TAGS="${mirrored[*]}" python3 - <<'PY'
90+import json, os, urllib.error, urllib.request
91+
92+src = os.environ.get("MIRROR_SRC_API", "https://diffinery.com/api/v3/repos/codician/diffinery")
93+dst = os.environ.get("MIRROR_DST_API", "https://api.github.com/repos/codician-team/diffinery")
94+tags = set(os.environ["MIRROR_TAGS"].split())
95+
96+def call(method, url, body=None, token=None):
97+ req = urllib.request.Request(url, method=method, data=None if body is None else json.dumps(body).encode())
98+ req.add_header("Accept", "application/vnd.github+json")
99+ req.add_header("User-Agent", "diffinery-mirror")
100+ req.add_header("Content-Type", "application/json")
101+ if token:
102+ req.add_header("Authorization", f"Bearer {token}")
103+ try:
104+ with urllib.request.urlopen(req, timeout=30) as r:
105+ return r.status, json.load(r)
106+ except urllib.error.HTTPError as e:
107+ return e.code, None
108+
109+token = os.environ["GITHUB_MIRROR_TOKEN"]
110+status, releases = call("GET", f"{src}/releases?per_page=100", token=os.environ.get("DIFFINERY_TOKEN"))
111+if status != 200:
112+ raise SystemExit(f"could not list Diffinery releases ({status})")
113+for r in releases:
114+ if r["draft"] or r["tag_name"] not in tags:
115+ continue
116+ want = {"name": r["name"], "body": r["body"] or "", "prerelease": r["prerelease"]}
117+ status, have = call("GET", f"{dst}/releases/tags/{r['tag_name']}", token=token)
118+ if status == 404:
119+ status, _ = call("POST", f"{dst}/releases", {"tag_name": r["tag_name"], **want}, token)
120+ print(f"release {r['tag_name']}: created ({status})")
121+ elif status == 200 and any(have.get(k) != v for k, v in want.items()):
122+ status, _ = call("PATCH", f"{dst}/releases/{have['id']}", want, token)
123+ print(f"release {r['tag_name']}: updated ({status})")
124+ elif status != 200:
125+ print(f"warning: could not read GitHub release {r['tag_name']} ({status})")
126+PY
127+fi