#!/usr/bin/env python3 # SPDX-License-Identifier: MIT # Copyright © 2022 Intel Corporation """Tool that automatically applies patches and tests them as possible.""" from __future__ import annotations import asyncio import sys import typing from pick import core import aiohttp async def revert() -> None: await reset('HEAD~') async def reset(to: str = 'HEAD') -> None: p = await asyncio.create_subprocess_exec( 'git', 'reset', '--hard', to, stdout=asyncio.subprocess.DEVNULL, stderr=asyncio.subprocess.DEVNULL, ) await p.wait() async def git_push(commit: typing.Optional[core.Commit], commits: typing.List[core.Commit], force: bool = False) -> None: cmd = ['git', 'push'] if force: cmd.append('-f') p = await asyncio.create_subprocess_exec( *cmd, stdout=asyncio.subprocess.DEVNULL, stderr=asyncio.subprocess.DEVNULL, ) if await p.wait() != 0: print(' Critical Error: failed to push to gitlib') sys.exit(1) async def set_need_manual_resolution(commit: Commit, commits: T.List[Commit], force_push: bool = True) -> None: commit.resolution = core.Resolution.MANUAL_RESOLUTION core.save(commits) await core.commit_state(message=f'Mark {commit.sha} as needing manual resolution') await git_push(commit, commits, force_push) async def main(loop: asyncio.BaseEventLoop) -> None: commits = await core.update_commits() new_commits = [c for c in commits if c.nominated and c.resolution is core.Resolution.UNRESOLVED] failed: typing.Set[str] = set() print(' Sanity testing', flush=True) p = await asyncio.create_subprocess_exec( 'meson', 'setup', '--reconfigure', 'builddir', stdout=asyncio.subprocess.DEVNULL, stderr=asyncio.subprocess.DEVNULL, ) if await p.wait() != 0: print('ERROR: sanity check failed!') sys.exit(2) with open('VERSION', 'r') as f: version = f.read().split('-')[0].strip() version = '.'.join(version.split('.')[:2]) url = 'https://gitlab.freedesktop.org/api/v4/projects/176/pipelines' params = { 'ref': f'staging/{version}', 'per_page': '1', } lock = asyncio.Lock() for commit in reversed(new_commits): async with lock: print(f'Commit: {commit.sha}: {commit.description}') if commit.because_sha in failed: # This isn't actually failed, but in a case like: # C requires B, B requires A, A fails to apply # We want C to be excluded as well failed.add(commit.sha) print(' Not applying because the commit it fixes was not applied successfully') continue result, _ = await commit.apply() if not result: failed.add(commit.sha) print(f' FAILED to apply: {commit.sha}: {commit.description}') await reset() await set_need_manual_resolution(commit, commits, force_push=False) continue print(' Compiling project', flush=True) # TODO: make builddir configureable? p = await asyncio.create_subprocess_exec( 'ninja', '-C', 'builddir', 'test', stdout=asyncio.subprocess.DEVNULL, stderr=asyncio.subprocess.DEVNULL, ) if await p.wait() != 0: failed.add(commit.sha) print(f' FAILED to compile: {commit.sha}: {commit.description}, reverting') await revert() await set_need_manual_resolution(commit, commits, force_push=False) continue print(' Pushing update to git', flush=True) # update the ocmmit log with merged so that we don't force push and # hide the gitlab pipeline resuilts. commit.resolution = core.Resolution.MERGED core.save(commits) await core.commit_state(amend=True) await git_push(commit, commits) print(' Waiting for for CI to finish: ', end='', flush=True) async with aiohttp.ClientSession(loop=loop) as session: async with session.get(url, params=params) as response: content = await response.json() id_ = content[0]['id'] while True: async with session.get(f'{url}/{id_}') as response: content = await response.json() status: str = content['status'] if status in {'created', 'waiting_for_resources', 'preparing', 'pending', 'running', 'scheduled'}: print('.', end='', flush=True) await asyncio.sleep(60) continue elif status == 'success': print(f'\n Successfully applied: {commit.sha}') break else: if status == 'failed': print(f'\n CI Failed: {commit.sha}') else: print(f'\n Unexpected CI status "{status}": {commit.sha}') failed.add(commit.sha) await revert() await set_need_manual_resolution(commit, commits) break sys.exit(0) if __name__ == "__main__": loop = asyncio.get_event_loop() loop.run_until_complete(main(loop))