From b0f9a4c56e96c26066223fb753c69f8e4b0f2277 Mon Sep 17 00:00:00 2001 From: Martin Pitt Date: Mon, 4 Mar 2024 14:13:08 +0100 Subject: [PATCH] tasks: Add local mock PR run to run-local.sh This is an "almost end to end" test which runs fully locally and requires no GitHub interaction or token. Add a little mock GitHub server (utilizing bots' `task.test_mock_server`) to respond to the required GitHub APIs for handling an "opened PR" webhook event. This is useful for playing around with our AMQP queue/job execution engine locally (when using the mock GH server in interactive mode), or validating changes to run-queue/AMQP structure. --- tasks/mock-github-pr | 85 ++++++++++++++++++++++++++++++++++++++++++++ tasks/run-local.sh | 41 +++++++++++++++++++++ 2 files changed, 126 insertions(+) create mode 100755 tasks/mock-github-pr diff --git a/tasks/mock-github-pr b/tasks/mock-github-pr new file mode 100755 index 00000000..bca7e67b --- /dev/null +++ b/tasks/mock-github-pr @@ -0,0 +1,85 @@ +#!/usr/bin/env python3 +# Mock GitHub API server for testing an opened PR +# You can run this manually in `tasks/run-local.sh -i` with: +# PYTHONPATH=. ./mock-github-pr cockpit-project/bots $(git rev-parse HEAD) & +# export GITHUB_API=http://127.0.0.7:8443 +# PYTHONPATH=. ./mock-github-pr --print-event cockpit-project/bots $(git rev-parse HEAD) | \ +# ./publish-queue --amqp localhost:5671 --queue webhook +# +# and then two `./run-queue --amqp localhost:5671` +# first to process webhook → tests-scan → public, second to actually run it + +import argparse +import json +import os +import tempfile + +from task.test_mock_server import MockHandler, MockServer + +repo = None +sha = None + + +class Handler(MockHandler): + def do_GET(self): + if self.path in self.server.data: + self.replyJson(self.server.data[self.path]) + elif self.path.startswith(f'/repos/{repo}/pulls?'): + self.replyJson([self.server.data[f'/repos/{repo}/pulls/1']]) + elif self.path == f'/{repo}/{sha}/.cockpit-ci/container': + self.replyData('quay.io/cockpit/tasks') + else: + self.send_error(404, 'Mock Not Found: ' + self.path) + + def do_POST(self): + if self.path.startswith(f'/repos/{repo}/statuses/{sha}'): + self.replyJson({}) + else: + self.send_error(405, 'Method not allowed: ' + self.path) + + +argparser = argparse.ArgumentParser() +argparser.add_argument('--port', type=int, default=8443, help="Port to listen on (default: %(default)s)") +argparser.add_argument('--print-event', action='store_true', help="Print GitHub webhook pull_request event and exit") +argparser.add_argument('repo', metavar='USER/PROJECT', help="GitHub user/org and project name") +argparser.add_argument('sha', help="SHA to test in repo for the mock PR") +args = argparser.parse_args() +repo = args.repo +sha = args.sha + +ADDRESS = ('127.0.0.7', args.port) + +GITHUB_DATA = { + f'/repos/{repo}/pulls/1': { + 'title': 'mock PR', + 'number': 1, + 'state': 'open', + 'body': "This is the body", + 'base': {'repo': {'full_name': repo}, 'ref': 'main'}, + 'head': {'sha': args.sha, 'user': {'login': repo.split('/')[0]}}, + 'labels': [], + 'updated_at': 0, + }, + f'/repos/{repo}/commits/{args.sha}/status?page=1&per_page=100': { + 'state': 'pending', + 'statuses': [], + 'sha': sha, + }, +} + +if args.print_event: + print(json.dumps({ + 'event': 'pull_request', + 'request': { + 'action': 'opened', + 'pull_request': GITHUB_DATA[f'/repos/{repo}/pulls/1'] + } + }, indent=4)) + exit(0) + +temp = tempfile.TemporaryDirectory() +cache_dir = os.path.join(temp.name, 'cache') +os.environ['XDG_CACHE_HOME'] = cache_dir +server = MockServer(ADDRESS, Handler, GITHUB_DATA) +server.start() +print(f'export GITHUB_API=http://{ADDRESS[0]}:{ADDRESS[1]}') diff --git a/tasks/run-local.sh b/tasks/run-local.sh index 22d1eaad..2e9837d7 100755 --- a/tasks/run-local.sh +++ b/tasks/run-local.sh @@ -238,6 +238,45 @@ test_image() { ' } +test_mock_pr() { + podman cp "$MYDIR/mock-github-pr" cockpituous-tasks:/work/bots/mock-github-pr + podman exec -i cockpituous-tasks sh -euxc " + cd bots + # test mock PR against our checkout, so that cloning will work + SHA=\$(git rev-parse HEAD) + + # start mock GH server + PYTHONPATH=. ./mock-github-pr cockpit-project/bots \$SHA & + GH_MOCK_PID=\$! + export GITHUB_API=http://127.0.0.7:8443 + until curl --silent \$GITHUB_API; do sleep 0.1; done + + # simulate GitHub webhook event, put that into the webhook queue + PYTHONPATH=. ./mock-github-pr --print-event cockpit-project/bots \$SHA | \ + ./publish-queue --amqp $AMQP_POD --queue webhook + + ./inspect-queue --amqp $AMQP_POD + + # first run-queue processes webhook → tests-scan → public queue + ./run-queue --amqp localhost:5671 + ./inspect-queue --amqp $AMQP_POD + + # second run-queue actually runs the test + ./run-queue --amqp localhost:5671 + + kill \$GH_MOCK_PID + " + + LOGS_URL="$S3_URL_HOST/logs/" + CURL="curl --cacert $SECRETS/ca.pem --silent --fail --show-error" + LOG_MATCH="$($CURL $LOGS_URL| grep -o "pull-1-[[:alnum:]-]*-unit-tests/log<")" && break + LOG="$($CURL "${LOGS_URL}${LOG_MATCH%<}")" + echo "--------------- mock PR test log -----------------" + echo "$LOG" + echo "--------------- mock PR test log end -------------" + assert_in 'Test run finished' "$LOG" +} + test_pr() { # need to use real GitHub token for this [ -z "$TOKEN" ] || cp -fv "$TOKEN" "$SECRETS"/webhook/.config--github-token @@ -310,6 +349,8 @@ else # tests which don't need GitHub interaction test_image test_queue + # "almost" end-to-end, starting with GitHub webhook JSON payload injection; fully localy, no privs + test_mock_pr # if we have a PR number, run a unit test inside local deployment, and update PR status [ -z "$PR" ] || test_pr fi