ci: automate reviewed upstream 8.0 proposals
This commit is contained in:
@@ -0,0 +1,41 @@
|
|||||||
|
name: Propose upstream Tryton updates
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: '35 */6 * * *'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
pull-requests: write
|
||||||
|
issues: write
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: upstream-8.0-sync
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
propose-update:
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
env:
|
||||||
|
GITEA_URL: https://code.unpaismejor.org.do
|
||||||
|
MIRROR_REPOSITORY: tryton-do/tryton-mirror
|
||||||
|
CUSTOM_REPOSITORY: tryton-do/tryton
|
||||||
|
TARGET_BRANCH: '8.0'
|
||||||
|
INTEGRATION_BRANCH: integration/8.0
|
||||||
|
UPDATE_BRANCH: automation/upstream-8.0
|
||||||
|
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||||
|
steps:
|
||||||
|
- name: Check out customized repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
token: ${{ secrets.GITEA_TOKEN }}
|
||||||
|
|
||||||
|
- name: Configure Git identity
|
||||||
|
run: |
|
||||||
|
git config user.name 'tryton-sync'
|
||||||
|
git config user.email 'tryton-sync@noreply.unpaismejor.org.do'
|
||||||
|
|
||||||
|
- name: Propose upstream update
|
||||||
|
run: ci/sync_upstream.sh
|
||||||
Executable
+63
@@ -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}")"
|
||||||
Executable
+91
@@ -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()
|
||||||
Reference in New Issue
Block a user