Files

138 lines
5.6 KiB
Python

"""固定同步 NoneBot main;默认离线 dry-run,普通提交保留历史。"""
import argparse
import base64
import json
import os
import re
from urllib.error import HTTPError
from urllib.request import Request, urlopen
from verify_nonebot import read_package, verify_files
from release_metadata import commit_message
REPOSITORY = "Shu-Ying/nonebot_plugin_farm"
BRANCH = "main"
def github(method, path, payload, token, allow_missing=False):
url = f"https://api.github.com/repos/{REPOSITORY}"
if path:
url += f"/{path}"
request = Request(
url,
data=json.dumps(payload).encode() if payload is not None else None,
headers={"Authorization": f"Bearer {token}",
"Accept": "application/vnd.github+json",
"X-GitHub-Api-Version": "2022-11-28",
"User-Agent": "farm-nonebot-publisher"}, method=method,
)
try:
with urlopen(request, timeout=30) as response:
return json.load(response)
except HTTPError as error:
if error.code == 404 and method == "GET" and allow_missing:
return None
raise RuntimeError(
f"GitHub request failed: {method} {url}: HTTP {error.code}",
) from None
def publish(package, confirmed=False, request=None, *, release_tag=False):
files = read_package(package)
manifest = verify_files(files)
if not re.fullmatch(r'[0-9]+\.[0-9]+\.[0-9]+(?:-[a-z0-9.-]+)?',
manifest['version']):
raise ValueError('Invalid release version in version.txt')
message = commit_message(manifest)
result = {"repository": REPOSITORY, "branch": BRANCH,
"version": manifest["version"], "message": message,
"published": False}
tag = 'v' + manifest['version']
if release_tag:
result.update(tag=tag, tag_created=False)
if not confirmed:
return result
if manifest["source"]["dirty"]:
raise ValueError("Publish requires a clean source checkout")
token = os.environ.get("FARM_RELEASE_TOKEN")
if not token:
raise ValueError("Configure FARM_RELEASE_TOKEN in a protected job")
call = request or (lambda method, path, data: github(
method, path, data, token,
allow_missing=path.startswith((
"git/ref/heads/", "git/ref/tags/", "contents/",
)),
))
if not call("GET", "", None):
raise ValueError(f"Target repository is not accessible: {REPOSITORY}")
if release_tag and call('GET', f'git/ref/tags/{tag}', None):
owned = call('GET', f'contents/FARM-MANIFEST.json?ref={tag}', None)
if owned is None or owned.get('encoding') != 'base64':
raise ValueError('Existing release is not managed; bump version.txt')
previous = json.loads(base64.b64decode(owned['content']))
if (previous.get('target') != 'nonebot'
or previous.get('plugin') != manifest['plugin']
or previous.get('version') != manifest['version']
or previous.get('files') != manifest['files']):
raise ValueError('Release tag has different files; bump version.txt')
return {**result, 'tag_reused': True}
ref = call("GET", f"git/ref/heads/{BRANCH}", None)
parent = ref["object"]["sha"] if ref else None
if parent:
owned = call("GET", f"contents/FARM-MANIFEST.json?ref={BRANCH}", None)
if owned is not None:
if owned.get("encoding") != "base64":
raise ValueError("Existing Farm manifest encoding is invalid")
previous = json.loads(base64.b64decode(owned["content"]))
if (previous.get("target") != "nonebot"
or previous.get("plugin") != manifest["plugin"]):
raise ValueError("Existing branch ownership differs")
tree = []
for name, content in sorted(files.items()):
blob = call("POST", "git/blobs", {
"content": base64.b64encode(content).decode(),
"encoding": "base64",
})
tree.append({"path": name, "mode": "100644", "type": "blob",
"sha": blob["sha"]})
generated = call("POST", "git/trees", {"tree": tree})
commit = call("POST", "git/commits", {
"message": message,
"tree": generated["sha"], "parents": [parent] if parent else [],
})
if parent:
current = call("GET", f"git/ref/heads/{BRANCH}", None)
if not current or current["object"]["sha"] != parent:
raise RuntimeError("Target branch changed; retry explicitly")
call("PATCH", f"git/refs/heads/{BRANCH}", {
"sha": commit["sha"], "force": False,
})
else:
call("POST", "git/refs", {
"ref": f"refs/heads/{BRANCH}", "sha": commit["sha"],
})
result.update(published=True, commit=commit['sha'])
if release_tag:
# 仅创建不可变标签;失败保留已同步main,重跑时重新检查,不移动旧标签。
call('POST', 'git/refs', {
'ref': f'refs/tags/{tag}', 'sha': commit['sha'],
})
result['tag_created'] = True
return result
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("package")
parser.add_argument("--publish", action="store_true")
parser.add_argument('--release-tag', action='store_true',
help='同步后创建v版本标签,触发目标仓库PyPI发布')
args = parser.parse_args()
print(json.dumps(publish(args.package, args.publish,
release_tag=args.release_tag), ensure_ascii=False))
if __name__ == "__main__":
main()