ci: automate reviewed upstream 8.0 proposals

This commit is contained in:
José Arturo García
2026-10-13 19:07:53 -04:00
parent 00c2302068
commit 9a16786c2d
3 changed files with 195 additions and 0 deletions
+63
View File
@@ -0,0 +1,63 @@
#!/usr/bin/env bash
set -Eeuo pipefail
required=(GITEA_URL MIRROR_REPOSITORY CUSTOM_REPOSITORY TARGET_BRANCH \
INTEGRATION_BRANCH UPDATE_BRANCH GITEA_TOKEN)
for variable in "${required[@]}"; do
if [[ -z "${!variable:-}" ]]; then
echo "Missing required environment variable: ${variable}" >&2
exit 2
fi
done
auth_header="Authorization: token ${GITEA_TOKEN}"
mirror_url="${GITEA_URL}/${MIRROR_REPOSITORY}.git"
push_url="${GITEA_URL}/${CUSTOM_REPOSITORY}.git"
git remote add upstream "${mirror_url}"
git -c "http.extraHeader=${auth_header}" fetch --no-tags upstream \
"+refs/heads/${TARGET_BRANCH}:refs/remotes/upstream/${TARGET_BRANCH}"
git fetch origin "${INTEGRATION_BRANCH}"
upstream_ref="upstream/${TARGET_BRANCH}"
base_ref="origin/${INTEGRATION_BRANCH}"
if git merge-base --is-ancestor "${upstream_ref}" "${base_ref}"; then
echo "${INTEGRATION_BRANCH} already contains the current upstream commit."
exit 0
fi
if git ls-remote --exit-code --heads origin "${UPDATE_BRANCH}" >/dev/null 2>&1; then
git fetch origin "${UPDATE_BRANCH}"
git switch --create "${UPDATE_BRANCH}" --track "origin/${UPDATE_BRANCH}"
else
git switch --create "${UPDATE_BRANCH}" "${base_ref}"
fi
# Keep a reused automation branch based on the latest integration history.
# This merge is intentionally non-destructive: conflicts create an issue and
# leave every published reference untouched.
if ! git merge-base --is-ancestor "${base_ref}" HEAD; then
if ! git merge --no-edit --no-ff "${base_ref}"; then
conflicts=$(git diff --name-only --diff-filter=U | sed '/^$/d')
git merge --abort
python ci/upstream_pr.py conflict "${conflicts}"
exit 1
fi
fi
if ! git merge --no-edit --no-ff "${upstream_ref}"; then
conflicts=$(git diff --name-only --diff-filter=U | sed '/^$/d')
git merge --abort
python ci/upstream_pr.py conflict "${conflicts}"
exit 1
fi
if git diff --quiet "origin/${UPDATE_BRANCH}" HEAD 2>/dev/null; then
echo 'The existing update branch already contains this upstream revision.'
else
git -c "http.extraHeader=${auth_header}" push "${push_url}" \
"HEAD:refs/heads/${UPDATE_BRANCH}"
fi
python ci/upstream_pr.py pull-request "$(git rev-parse "${upstream_ref}")"
+91
View File
@@ -0,0 +1,91 @@
#!/usr/bin/env python3
"""Create an idempotent Gitea pull request or conflict issue."""
from __future__ import annotations
import json
import os
import sys
import urllib.error
import urllib.parse
import urllib.request
BASE_URL = os.environ['GITEA_URL'].rstrip('/') + '/api/v1'
REPOSITORY = os.environ['CUSTOM_REPOSITORY']
TOKEN = os.environ['GITEA_TOKEN']
TARGET = os.environ['INTEGRATION_BRANCH']
HEAD = os.environ['UPDATE_BRANCH']
def request(method: str, path: str, payload: dict | None = None):
data = json.dumps(payload).encode() if payload is not None else None
req = urllib.request.Request(
f'{BASE_URL}{path}', data=data, method=method,
headers={
'Authorization': f'token {TOKEN}',
'Accept': 'application/json',
'Content-Type': 'application/json',
})
try:
with urllib.request.urlopen(req, timeout=30) as response:
return json.load(response)
except urllib.error.HTTPError as exc:
detail = exc.read().decode(errors='replace')
raise RuntimeError(f'Gitea API returned {exc.code}: {detail}') from exc
except urllib.error.URLError as exc:
raise RuntimeError(f'Could not reach the Gitea API: {exc.reason}') from exc
def open_pulls():
query = urllib.parse.urlencode({'state': 'open', 'limit': 50})
return request('GET', f'/repos/{REPOSITORY}/pulls?{query}')
def ensure_pull_request(upstream_sha: str) -> None:
for pull in open_pulls():
if pull['head']['ref'] == HEAD and pull['base']['ref'] == TARGET:
print(f"Using existing pull request: {pull['html_url']}")
return
pull = request('POST', f'/repos/{REPOSITORY}/pulls', {
'title': f'Upstream Tryton 8.0 update ({upstream_sha[:12]})',
'head': HEAD,
'base': TARGET,
'body': (
'Automated proposal from the private official mirror.\n\n'
'Review the upstream changes and wait for all required checks. '
'This pull request must be merged manually.'),
})
print(f"Created pull request: {pull['html_url']}")
def ensure_conflict_issue(conflicts: str) -> None:
title = 'Upstream Tryton 8.0 synchronization conflict'
query = urllib.parse.urlencode({'state': 'open', 'type': 'issues', 'limit': 50})
for issue in request('GET', f'/repos/{REPOSITORY}/issues?{query}'):
if issue['title'] == title:
print(f"Conflict issue already open: {issue['html_url']}")
return
issue = request('POST', f'/repos/{REPOSITORY}/issues', {
'title': title,
'body': (
'The scheduled synchronization could not merge the latest '
'official 8.0 changes. No branch was overwritten.\n\n'
f'Conflicting paths:\n```\n{conflicts or "unknown"}\n```'),
})
print(f"Created conflict issue: {issue['html_url']}")
def main() -> None:
if len(sys.argv) != 3:
raise SystemExit('usage: upstream_pr.py pull-request SHA | conflict PATHS')
if sys.argv[1] == 'pull-request':
ensure_pull_request(sys.argv[2])
elif sys.argv[1] == 'conflict':
ensure_conflict_issue(sys.argv[2])
else:
raise SystemExit('usage: upstream_pr.py pull-request SHA | conflict PATHS')
if __name__ == '__main__':
main()