diff --git a/.github/workflows/bedevere.yml b/.github/workflows/bedevere.yml new file mode 100644 index 00000000..bbde255d --- /dev/null +++ b/.github/workflows/bedevere.yml @@ -0,0 +1,49 @@ +name: Bedevere + +on: + pull_request: + types: + - opened + - synchronize + - reopened + - edited + - labeled + - unlabeled + - review_requested + - ready_for_review + - converted_to_draft + - closed + create: + types: + - branch + pull_request_review: + types: + - submitted + - dismissed + issue_comment: + types: + - created + push: + +jobs: + build: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: [ "3.11" ] + steps: + - name: Check out repo + uses: actions/checkout@v3 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install -r requirements.txt + - name: execute bedevere + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + python -m bedevere diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..30c4b0fe --- /dev/null +++ b/Dockerfile @@ -0,0 +1,13 @@ +FROM python:3.11-slim + +COPY requirements.txt requirements.txt +COPY dev-requirements.txt dev-requirements.txt + + +COPY entrypoint.sh /entrypoint.sh +COPY bedevere/ /bedevere/ + +RUN pip install --no-cache-dir -U pip +RUN pip install --no-cache-dir -r requirements.txt + +ENTRYPOINT ["/entrypoint.sh"] diff --git a/Procfile b/Procfile deleted file mode 100644 index bda30287..00000000 --- a/Procfile +++ /dev/null @@ -1 +0,0 @@ -web: python3 -m bedevere diff --git a/action.yml b/action.yml new file mode 100644 index 00000000..76cb8a14 --- /dev/null +++ b/action.yml @@ -0,0 +1,10 @@ +--- +name: bedevere-action +description: Bedevere GitHub Actions + +branding: + color: blue + icon: activity +runs: + using: docker + image: Dockerfile diff --git a/bedevere/__main__.py b/bedevere/__main__.py index 3a3c0740..dc3e3827 100644 --- a/bedevere/__main__.py +++ b/bedevere/__main__.py @@ -1,5 +1,5 @@ import asyncio -import importlib + import os import sys import traceback @@ -10,27 +10,30 @@ from gidgethub import aiohttp as gh_aiohttp from gidgethub import routing from gidgethub import sansio +from gidgethub import actions from . import backport, gh_issue, close_pr, filepaths, news, stage -import sentry_sdk +# import sentry_sdk router = routing.Router(backport.router, gh_issue.router, close_pr.router, filepaths.router, news.router, stage.router) cache = cachetools.LRUCache(maxsize=500) -sentry_sdk.init(os.environ.get("SENTRY_DSN")) +# sentry_sdk.init(os.environ.get("SENTRY_DSN")) -async def main(request): +async def main(event_payload): try: - body = await request.read() - secret = os.environ.get("GH_SECRET") - event = sansio.Event.from_http(request.headers, body, secret=secret) + event_name = os.environ["GITHUB_EVENT_NAME"] + job_id = os.environ["GITHUB_JOB"] + event = sansio.Event(event_payload, event=event_name, delivery_id=job_id) + print(f"{event.data=}") + print(f"{event_payload}") + print(f"{event_name=}") print('GH delivery ID', event.delivery_id, file=sys.stderr) - if event.event == "ping": - return web.Response(status=200) - oauth_token = os.environ.get("GH_AUTH") + + oauth_token = os.environ.get("GITHUB_TOKEN") async with aiohttp.ClientSession() as session: gh = gh_aiohttp.GitHubAPI(session, "python/bedevere", oauth_token=oauth_token, @@ -42,16 +45,14 @@ async def main(request): print('GH requests remaining:', gh.rate_limit.remaining) except AttributeError: pass - return web.Response(status=200) + except Exception as exc: traceback.print_exc(file=sys.stderr) - return web.Response(status=500) - + sys.exit(1) if __name__ == "__main__": # pragma: no cover - app = web.Application() - app.router.add_post("/", main) - port = os.environ.get("PORT") - if port is not None: - port = int(port) - web.run_app(app, port=port) + if os.environ.get("GITHUB_EVENT_PATH"): + event_from = actions.event() + asyncio.run(main(event_from)) + else: + print(f"Environment Variable 'GITHUB_EVENT_PATH' not found.") diff --git a/bedevere/news.py b/bedevere/news.py index 1d9c30aa..6edebada 100644 --- a/bedevere/news.py +++ b/bedevere/news.py @@ -40,6 +40,7 @@ async def check_news(gh, pull_request, files=None): The routing is handled through the filepaths module. """ + print(f"Check news {pull_request=}") if not files: files = await util.files_for_PR(gh, pull_request) in_next_dir = file_found = False diff --git a/bedevere/stage.py b/bedevere/stage.py index f3914d9b..b5ec3d64 100644 --- a/bedevere/stage.py +++ b/bedevere/stage.py @@ -146,7 +146,8 @@ async def new_commit_pushed(event, gh, *arg, **kwargs): if len(commits) > 0: # get the latest commit hash commit_hash = commits[-1]["id"] - pr = await util.get_pr_for_commit(gh, commit_hash) + repo_full_name = event.data["repository"]["full_name"] + pr = await util.get_pr_for_commit(gh, commit_hash, repo_full_name) for label in util.labels(pr): if label == "awaiting merge": issue = await util.issue_for_PR(gh, pr) diff --git a/bedevere/util.py b/bedevere/util.py index 84305d4f..e86739fe 100644 --- a/bedevere/util.py +++ b/bedevere/util.py @@ -68,6 +68,7 @@ def create_status(context, state, *, description=None, target_url=None): async def post_status(gh, event, status): """Post a status in reaction to an event.""" + print(f"Post status {event.data=}") await gh.post(event.data["pull_request"]["statuses_url"], data=status) @@ -227,10 +228,12 @@ def no_labels(event_data): return False -async def get_pr_for_commit(gh, sha): +async def get_pr_for_commit(gh, sha, repo_full_name=None): """Find the PR containing the specific commit hash.""" + if not repo_full_name: + repo_full_name = "python/cpython" prs_for_commit = await gh.getitem( - f"/search/issues?q=type:pr+repo:python/cpython+sha:{sha}" + f"/search/issues?q=type:pr+repo:{repo_full_name}+sha:{sha}" ) if prs_for_commit["total_count"] > 0: # there should only be one return prs_for_commit["items"][0] diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100755 index 00000000..6ca740c6 --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,5 @@ +#!/bin/sh -l + +cp -r /bedevere ./bedevere + +python -m bedevere \ No newline at end of file diff --git a/runtime.txt b/runtime.txt deleted file mode 100644 index a5da7cc4..00000000 --- a/runtime.txt +++ /dev/null @@ -1 +0,0 @@ -python-3.10.5 diff --git a/tests/fixtures.py b/tests/fixtures.py new file mode 100644 index 00000000..baee2f36 --- /dev/null +++ b/tests/fixtures.py @@ -0,0 +1,18 @@ +import os +import pytest + + +@pytest.fixture +def tmp_event_name(request, monkeypatch): + monkeypatch.setenv("GITHUB_EVENT_NAME", request.param) + +@pytest.fixture +def tmp_job_id(monkeypatch): + monkeypatch.setenv("GITHUB_JOB", "12345") + +@pytest.fixture +def tmp_webhook(tmp_path, monkeypatch): + """Create a temporary file for an actions webhook event.""" + tmp_file_path = tmp_path / "event.json" + monkeypatch.setenv("GITHUB_EVENT_PATH", os.fspath(tmp_file_path)) + return tmp_file_path diff --git a/tests/test___main__.py b/tests/test___main__.py index e9b49172..ac6e78cc 100644 --- a/tests/test___main__.py +++ b/tests/test___main__.py @@ -1,38 +1,25 @@ -from aiohttp import web import pytest -from bedevere import __main__ as main +from tests.fixtures import tmp_webhook, tmp_event_name, tmp_job_id -async def test_ping(aiohttp_client): - app = web.Application() - app.router.add_post("/", main.main) - client = await aiohttp_client(app) - headers = {"x-github-event": "ping", - "x-github-delivery": "1234"} - data = {"zen": "testing is good"} - response = await client.post("/", headers=headers, json=data) - assert response.status == 200 +@pytest.mark.parametrize('tmp_event_name', ["created"], indirect=True) +async def test_success(tmp_webhook, tmp_event_name, tmp_job_id, monkeypatch): + from bedevere import __main__ as main -async def test_success(aiohttp_client): - app = web.Application() - app.router.add_post("/", main.main) - client = await aiohttp_client(app) - headers = {"x-github-event": "project", - "x-github-delivery": "1234"} # Sending a payload that shouldn't trigger any networking, but no errors # either. - data = {"action": "created"} - response = await client.post("/", headers=headers, json=data) - assert response.status == 200 - - -async def test_failure(aiohttp_client): - """Even in the face of an exception, the server should not crash.""" - app = web.Application() - app.router.add_post("/", main.main) - client = await aiohttp_client(app) - # Missing key headers. - response = await client.post("/", headers={}) - assert response.status == 500 + event_payload = {"action": "created"} + await main.main(event_payload) + + +async def test_failure(tmp_webhook): + from bedevere import __main__ as main + """Actions will have exit code in case of errors.""" + + # Missing GitHub environment variables + event_payload = {} + with pytest.raises(SystemExit) as exc: + await main.main(event_payload) + assert exc.value.code == 1 diff --git a/tests/test_stage.py b/tests/test_stage.py index e5eaf072..b43a80e0 100644 --- a/tests/test_stage.py +++ b/tests/test_stage.py @@ -1097,16 +1097,19 @@ async def test_awaiting_label_not_removed_when_pr_not_merged(label): assert gh.delete_url is None -async def test_new_commit_pushed_to_approved_pr(): +@pytest.mark.parametrize("repo_full_name", ["python/cpython", "mariatta/cpython"]) +async def test_new_commit_pushed_to_approved_pr(repo_full_name): # There is new commit on approved PR username = "brettcannon" sha = "f2393593c99dd2d3ab8bfab6fcc5ddee540518a9" - data = {"commits": [{"id": sha}]} + data = {"commits": [{"id": sha}], + "repository": {"full_name": repo_full_name}, + } event = sansio.Event(data, event="push", delivery_id="12345") teams = [{"name": "python core", "id": 6}] items = { f"https://api.github.com/teams/6/memberships/{username}": "OK", - f"https://api.github.com/search/issues?q=type:pr+repo:python/cpython+sha:{sha}": { + f"https://api.github.com/search/issues?q=type:pr+repo:{repo_full_name}+sha:{sha}": { "total_count": 1, "items": [ { @@ -1169,13 +1172,15 @@ async def test_new_commit_pushed_to_approved_pr(): } -async def test_new_commit_pushed_to_not_approved_pr(): +@pytest.mark.parametrize("repo_full_name", ["python/cpython", "mariatta/cpython"]) +async def test_new_commit_pushed_to_not_approved_pr(repo_full_name): # There is new commit on approved PR sha = "f2393593c99dd2d3ab8bfab6fcc5ddee540518a9" - data = {"commits": [{"id": sha}]} + data = {"commits": [{"id": sha}], + "repository": {"full_name": repo_full_name},} event = sansio.Event(data, event="push", delivery_id="12345") items = { - f"https://api.github.com/search/issues?q=type:pr+repo:python/cpython+sha:{sha}": { + f"https://api.github.com/search/issues?q=type:pr+repo:{repo_full_name}+sha:{sha}": { "total_count": 1, "items": [ {