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
- CI / Typecheck and bundlesuccess
- CI / Unit tests (1/2)success
- CI / Unit tests (2/2)success
- Deploy / Deploy to Cloudflaresuccess
- Deploy / Main's current tipsuccess
- End-to-end / End-to-end (agent reviews, tasks, Today)success
- End-to-end / End-to-end (git, PRs, Actions)success
- End-to-end / End-to-end (large pushes)success
- End-to-end / End-to-end (passkeys, registry, email)success
- End-to-end / End-to-end (real histories)success
- End-to-end / End-to-end (runner Docker)success
- End-to-end / End-to-end (security)success
- Live test / Containers, registry, sandboxes, webhooks, schedules, LFS and pull requestssuccess
- Mirror to GitHub / Push main to the GitHub mirrorsuccess
1 file changed+64−5
modifiedscripts/mirror-to-github.sh+64−5View file
| @@ -6,7 +6,8 @@ | ||
| 6 | 6 | # personal contributors. |
| 7 | 7 | # The rewrite is deterministic: the same Diffinery history always gives the same |
| 8 | 8 | # 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. | |
| 10 | 11 | # |
| 11 | 12 | # Reads Diffinery with DIFFINERY_TOKEN when set (not needed once the repo is public); |
| 12 | 13 | # MIRROR_SRC can also be a local clone that has main. Pushes with GITHUB_MIRROR_TOKEN |
| @@ -22,7 +23,7 @@ | ||
| 22 | 23 | SRC=${MIRROR_SRC:-https://diffinery.com/codician/diffinery.git} |
| 23 | 24 | DST=${MIRROR_DST:-https://github.com/codician-team/diffinery.git} |
| 24 | 25 | NAME=${MIRROR_NAME:-codician-team} |
| 25 | -EMAIL=${MIRROR_EMAIL:-codician-team@users.noreply.diffinery.com} | |
| 26 | +EMAIL=${MIRROR_EMAIL:-hi@codician.com} | |
| 26 | 27 | dry_run=${1:-} |
| 27 | 28 | |
| 28 | 29 | work=${MIRROR_WORKDIR:-} |
| @@ -62,7 +63,65 @@ | ||
| 62 | 63 | fi |
| 63 | 64 | remote=$(git "${auth[@]}" ls-remote "$DST" refs/heads/main | cut -f1) |
| 64 | 65 | 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 | |
| 67 | 69 | 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 |