diff --git a/job-runner.toml b/job-runner.toml index 7f5d877f63..7ecb8dbabf 100644 --- a/job-runner.toml +++ b/job-runner.toml @@ -50,6 +50,7 @@ driver='github' [forge.github] clone-url = 'https://github.com/' api-url = 'https://api.github.com/' +content-url = 'https://raw.githubusercontent.com/' post = true # whether to post statuses, open issues, etc. user-agent = 'job-runner (cockpit-project/bots)' # (at least) one of `token` or `post = false` must be set diff --git a/lib/aio/abc.py b/lib/aio/abc.py index 8cc838b05d..3ecf05708d 100644 --- a/lib/aio/abc.py +++ b/lib/aio/abc.py @@ -18,14 +18,32 @@ from yarl import URL -from .jsonutil import JsonObject +from .jsonutil import JsonObject, get_int, get_str + + +class SubjectSpecification: + def __init__(self, obj: JsonObject) -> None: + self.repo = get_str(obj, 'repo') + self.sha = get_str(obj, 'sha', None) + self.pull = get_int(obj, 'pull', None) + self.branch = get_str(obj, 'branch', None) + self.target = get_str(obj, 'target', None) class Subject(NamedTuple): - clone_url: URL + forge: 'Forge' + repo: str sha: str rebase: str | None = None + @property + def clone_url(self) -> URL: + return self.forge.clone / self.repo + + @property + def content_url(self) -> URL: + return self.forge.content / self.repo + class Status: link: str @@ -35,9 +53,10 @@ async def post(self, state: str, description: str) -> None: class Forge: - async def resolve_subject( - self, repo: str, sha: str | None, pull_nr: int | None, branch: str | None, target: str | None - ) -> Subject: + clone: URL + content: URL + + async def resolve_subject(self, spec: SubjectSpecification) -> Subject: raise NotImplementedError async def check_pr_changed(self, repo: str, pull_nr: int, expected_sha: str) -> str | None: @@ -49,6 +68,9 @@ def get_status(self, repo: str, sha: str, context: str | None, location: URL) -> async def open_issue(self, repo: str, issue: JsonObject) -> None: raise NotImplementedError + async def read_file(self, subject: Subject, filename: str) -> str | None: + raise NotImplementedError + @classmethod def new(cls, config: JsonObject) -> Self: raise NotImplementedError diff --git a/lib/aio/github.py b/lib/aio/github.py index fa477a2438..a25b2ea4fc 100644 --- a/lib/aio/github.py +++ b/lib/aio/github.py @@ -19,18 +19,35 @@ import logging import platform from collections.abc import Mapping -from typing import NamedTuple, Self +from typing import Awaitable, Callable, NamedTuple, Self import aiohttp from yarl import URL -from .abc import Forge, Status, Subject +from .abc import Forge, Status, Subject, SubjectSpecification from .jsonutil import JsonError, JsonObject, JsonValue, get_bool, get_dict, get_nested, get_str, typechecked -from .util import LRUCache, create_http_session +from .util import LRUCache, T, create_http_session logger = logging.getLogger(__name__) +async def retry(func: Callable[[], Awaitable[T]]) -> T: + for attempt in range(4): + try: + return await func() + except aiohttp.ClientResponseError as exc: + if exc.status < 500: + raise + except aiohttp.ClientError: + pass + + # 1 → 2 → 4 → 8s delay + await asyncio.sleep(2 ** attempt) + + # ...last attempt. + return await func() + + class CacheEntry(NamedTuple): conditions: Mapping[str, str] value: JsonValue @@ -43,6 +60,7 @@ def __init__(self, config: JsonObject) -> None: self.config = config self.clone = URL(get_str(config, 'clone-url')) self.api = URL(get_str(config, 'api-url')) + self.content = URL(get_str(config, 'content-url')) self.dry_run = not get_bool(config, 'post') async def __aenter__(self) -> Self: @@ -65,20 +83,7 @@ async def post_once() -> JsonValue: logger.debug('response %r', response) return await response.json() # type: ignore[no-any-return] - for attempt in range(4): - try: - return await post_once() - except aiohttp.ClientResponseError as exc: - if exc.status < 500: - raise - except aiohttp.ClientError: - pass - - # 1 → 2 → 4 → 8s delay - await asyncio.sleep(2 ** attempt) - - # ...last attempt. - return await post_once() + return await retry(post_once) async def get(self, resource: str) -> JsonValue: async def get_once() -> JsonValue: @@ -103,20 +108,7 @@ async def get_once() -> JsonValue: self.cache.add(resource, CacheEntry(conditions, value)) return value # type: ignore[no-any-return] - for attempt in range(4): - try: - return await get_once() - except aiohttp.ClientResponseError as exc: - if exc.status < 500: - raise - except aiohttp.ClientError: - pass - - # 1 → 2 → 4 → 8s delay - await asyncio.sleep(2 ** attempt) - - # ...last attempt. - return await get_once() + return await retry(get_once) async def get_obj(self, resource: str) -> JsonObject: return typechecked(await self.get(resource), dict) @@ -140,31 +132,38 @@ async def check_pr_changed(self, repo: str, pull_nr: int, expected_sha: str) -> async def open_issue(self, repo: str, issue: JsonObject) -> None: await self.post(f'repos/{repo}/issues', issue) + async def read_file(self, subject: Subject, filename: str) -> str | None: + async def read_once() -> str | None: + try: + async with self.session.get(self.content / subject.repo / subject.sha / filename) as response: + logger.debug('response %r', response) + return await response.text() + except aiohttp.ClientResponseError as exc: + if exc.status == 404: + return None + raise + + return await retry(read_once) + def get_status(self, repo: str, sha: str, context: str | None, location: URL) -> Status: return GitHubStatus(self, repo, sha, context, location) - async def resolve_subject( - self, repo: str, sha: str | None, pull_nr: int | None, branch: str | None, target: str | None - ) -> Subject: - clone_url = self.clone / repo + async def resolve_subject(self, spec: SubjectSpecification) -> Subject: + if spec.pull is not None: + pull = await self.get_obj(f'repos/{spec.repo}/pulls/{spec.pull}') + return Subject(self, spec.repo, + # mypy needs some help here. See https://github.com/python/mypy/issues/16659 + spec.sha if spec.sha else get_str(get_dict(pull, 'head'), 'sha'), + spec.target or get_str(get_dict(pull, 'base'), 'ref')) - if sha is not None: - # if pull_nr is set and our sha doesn't match the PR, we will - # detect it soon - return Subject(clone_url, sha, target) - - elif pull_nr is not None: - pull = await self.get_obj(f'repos/{repo}/pulls/{pull_nr}') - if target is None: - target = get_str(get_dict(pull, 'base'), 'ref') - return Subject(clone_url, get_str(get_dict(pull, 'head'), 'sha'), target) + elif spec.sha is not None: + return Subject(self, spec.repo, spec.sha, spec.target) else: - if not branch: - branch = get_str(await self.get_obj(f'repos/{repo}'), 'default_branch') + branch = spec.branch or get_str(await self.get_obj(f'repos/{spec.repo}'), 'default_branch') - with get_nested(await self.get_obj(f'repos/{repo}/git/refs/heads/{branch}'), 'object') as object: - return Subject(clone_url, get_str(object, 'sha'), target) + with get_nested(await self.get_obj(f'repos/{spec.repo}/git/refs/heads/{branch}'), 'object') as object: + return Subject(self, spec.repo, get_str(object, 'sha'), spec.target) class GitHubStatus(Status): diff --git a/lib/aio/job.py b/lib/aio/job.py index a48931ae79..53ac180f96 100644 --- a/lib/aio/job.py +++ b/lib/aio/job.py @@ -28,9 +28,9 @@ from typing import Never from ..constants import BOTS_DIR -from .abc import Forge, Subject +from .abc import Forge, Subject, SubjectSpecification from .jobcontext import JobContext -from .jsonutil import JsonObject, get_dict, get_int, get_str, get_str_map, get_strv +from .jsonutil import JsonObject, get_dict, get_int, get_object, get_str, get_str_map, get_strv from .s3streamer import Index, LogStreamer from .spawn import run, spawn from .util import gather_and_cancel, read_utf8 @@ -45,14 +45,11 @@ class Failure(Exception): class Job: def __init__(self, obj: JsonObject) -> None: # test subject specification - self.repo = get_str(obj, 'repo') - self.sha = get_str(obj, 'sha', None) - self.pull = get_int(obj, 'pull', None) - self.branch = get_str(obj, 'branch', None) - self.target = get_str(obj, 'target', None) + self.subject = SubjectSpecification(obj) # test specification self.container = get_str(obj, 'container', None) + self.command_subject = get_object(obj, 'command-subject', SubjectSpecification, None) self.secrets = get_strv(obj, 'secrets', ()) self.command = get_strv(obj, 'command', None) self.env = get_str_map(obj, 'env', {}) @@ -83,6 +80,14 @@ async def run_container(job: Job, subject: Subject, ctx: JobContext, log: LogStr cidfile = tmpdir / 'cidfile' attachments = tmpdir / 'attachments' + container_image = ( + job.container or + await ctx.forge.read_file(subject, '.cockpit-ci/container') or + ctx.default_image + ).strip() + + log.write(f'Using container image: {container_image}\n') + args = [ *ctx.container_cmd, 'run', # we run arbitrary commands in that container, which aren't prepared for being pid 1; reap zombies @@ -94,7 +99,7 @@ async def run_container(job: Job, subject: Subject, ctx: JobContext, log: LogStr f'--env=COCKPIT_CI_LOG_URL={log.url}', *itertools.chain.from_iterable(args for name, args in ctx.secrets_args.items() if name in job.secrets), - job.container or ctx.default_image, + container_image, # we might be using podman-remote, so we can't --volume this: 'python3', '-c', Path(f'{BOTS_DIR}/checkout-and-run').read_text(), # lulz @@ -150,28 +155,36 @@ async def run_container(job: Job, subject: Subject, ctx: JobContext, log: LogStr async def run_job(job: Job, ctx: JobContext) -> None: - subject = await ctx.forge.resolve_subject(job.repo, job.sha, job.pull, job.branch, job.target) - title = job.title or f'{job.context}@{job.repo}#{subject.sha[:12]}' - slug = job.slug or f'{job.repo}/{job.context or "-"}/{subject.sha[:12]}' + subject = await ctx.forge.resolve_subject(job.subject) + title = job.title or f'{job.context}@{job.subject.repo}#{subject.sha[:12]}' + slug = job.slug or f'{job.subject.repo}/{job.context or "-"}/{subject.sha[:12]}' async with ctx.logs.get_destination(slug) as destination: index = Index(destination) log = LogStreamer(index) - status = ctx.forge.get_status(job.repo, subject.sha, job.context, log.url) + status = ctx.forge.get_status(job.subject.repo, subject.sha, job.context, log.url) logger.info('Log: %s', log.url) try: - log.start(f'{title}\nRunning on: {platform.node()}\n\nJob(' + json.dumps(job.__dict__, indent=4) + ')\n') + log.start( + f'{title}\n\n' + f'Running on: {platform.node()}\n\n' + f'Job({json.dumps(job, default=lambda obj: obj.__dict__, indent=4)})\n\n' + ) await status.post('pending', 'In progress') - tasks = {run_container(job, subject, ctx, log)} + if job.command_subject is not None: + command_subject = await ctx.forge.resolve_subject(job.command_subject) + else: + command_subject = subject + tasks = {run_container(job, command_subject, ctx, log)} if job.timeout: tasks.add(timeout_minutes(job.timeout)) - if job.pull: - tasks.add(poll_pr(ctx.forge, job.repo, job.pull, subject.sha)) + if job.subject.pull is not None: + tasks.add(poll_pr(ctx.forge, job.subject.repo, job.subject.pull, subject.sha)) await gather_and_cancel(tasks) @@ -189,7 +202,7 @@ async def run_job(job: Job, ctx: JobContext) -> None: """).lstrip(), **job.report } - await ctx.forge.open_issue(job.repo, issue) + await ctx.forge.open_issue(job.subject.repo, issue) except asyncio.CancelledError: await status.post('error', 'Cancelled') diff --git a/lib/aio/jobcontext.py b/lib/aio/jobcontext.py index 3b5434ae33..580081c051 100644 --- a/lib/aio/jobcontext.py +++ b/lib/aio/jobcontext.py @@ -68,6 +68,7 @@ def load_config(self, path: Path, name: str, *, missing_ok: bool = False) -> Non except FileNotFoundError as exc: if missing_ok: logger.debug('No %s configuration found at %s', name, str(path)) + return else: sys.exit(f'{path}: {exc}') except OSError as exc: diff --git a/lib/aio/jsonutil.py b/lib/aio/jsonutil.py index 100490acc1..07698bf924 100644 --- a/lib/aio/jsonutil.py +++ b/lib/aio/jsonutil.py @@ -83,6 +83,15 @@ def get_dict(obj: JsonObject, key: str, default: DT | _Empty = _empty) -> DT | J return _get(obj, lambda v: typechecked(v, dict), key, default) +def get_object( + obj: JsonObject, + key: str, + constructor: Callable[[JsonObject], T], + default: Union[DT, _Empty] = _empty +) -> Union[DT, T]: + return _get(obj, lambda v: constructor(typechecked(v, dict)), key, default) + + def get_str_map(obj: JsonObject, key: str, default: DT | _Empty = _empty) -> DT | Mapping[str, str]: def as_str_map(value: JsonValue) -> Mapping[str, str]: return {key: typechecked(value, str) for key, value in typechecked(value, dict).items()} diff --git a/test/test_aio.py b/test/test_aio.py index db9091629b..eada7babb0 100644 --- a/test/test_aio.py +++ b/test/test_aio.py @@ -10,6 +10,7 @@ from aioresponses import CallbackResult, aioresponses from yarl import URL +from lib.aio.abc import SubjectSpecification from lib.aio.github import GitHub from lib.aio.jobcontext import JobContext from lib.aio.jsonutil import JsonObject, JsonValue, json_merge_patch @@ -20,6 +21,7 @@ class GitHubService: CLONE_URL = URL('http://github.test/') API_URL = URL('http://api.github.test/') + CONTENT_URL = URL('http://content.github.test/') TOKEN = 'token_ABCDEFG' USER_AGENT = __file__ # or any magic unique string @@ -33,6 +35,7 @@ def __init__(self) -> None: self.config: JsonObject = { 'api-url': str(self.API_URL), 'clone-url': str(self.CLONE_URL), + 'content-url': str(self.CONTENT_URL), 'post': True, 'token': self.TOKEN, 'user-agent': self.USER_AGENT, @@ -219,8 +222,8 @@ async def test_github_pr_lookup(service: GitHubService, api: GitHub) -> None: }) # Look up the sha in the PR via the REST API - subject = await api.resolve_subject('owner/repo', None, pull_nr, None, None) - assert subject == (service.CLONE_URL / repo, sha, 'main') + subject = await api.resolve_subject(SubjectSpecification({'repo': 'owner/repo', 'pull': pull_nr})) + assert subject == (api, repo, sha, 'main') service.assert_hits(1, 1) # The next thing that happens is that we poll this API a lot diff --git a/test/test_tests_scan.py b/test/test_tests_scan.py index c0c96f93d9..1197d388b6 100755 --- a/test/test_tests_scan.py +++ b/test/test_tests_scan.py @@ -82,8 +82,6 @@ def do_GET(self): elif self.path.endswith("/issues"): issues = self.server.data.get('issues', []) self.replyJson(issues) - elif self.path == "/project/repo/abcdef/.cockpit-ci/container": - self.replyData("supertasks") else: self.send_error(404, 'Mock Not Found: ' + self.path) @@ -268,7 +266,7 @@ def test_amqp_pr(self, mock_queue, _mock_strftime): " COCKPIT_BOTS_REF=main TEST_SCENARIO=nightly ../tests-invoke --pull-number" f" {self.pull_number} --revision {self.revision} --repo {self.repo}\"") - self.assertEqual(request, { + assert request == { "command": expected_command, "type": "test", "sha": "abcdef", @@ -278,10 +276,10 @@ def test_amqp_pr(self, mock_queue, _mock_strftime): "context": "fedora/nightly", "repo": "project/repo", "pull": int(self.pull_number), + "report": None, "sha": "abcdef", "slug": f"pull-{self.pull_number}-abcdef-20240102-030405-fedora-nightly", - "target": "stable-1.0", - "container": "supertasks", + "command-subject": None, "secrets": ["github-token", "image-download"], "env": { "BASE_BRANCH": "stable-1.0", @@ -290,7 +288,7 @@ def test_amqp_pr(self, mock_queue, _mock_strftime): "TEST_SCENARIO": "nightly", } } - }) + } # mock time for predictable test name @unittest.mock.patch("time.strftime", return_value="20240102-030405") @@ -320,7 +318,7 @@ def test_amqp_sha_nightly(self, mock_queue, _mock_strftime): 'cd make-checkout-workdir && TEST_OS=fedora COCKPIT_BOTS_REF=main ' 'TEST_SCENARIO=nightly ../tests-invoke --revision 9988aa --repo project/repo"') self.maxDiff = None - self.assertEqual(request, { + assert request == { "command": expected_command, "type": "test", "sha": "9988aa", @@ -331,13 +329,12 @@ def test_amqp_sha_nightly(self, mock_queue, _mock_strftime): "repo": "project/repo", "sha": "9988aa", "slug": "pull-0-9988aa-20240102-030405-fedora-nightly", - "target": None, + "pull": None, "report": { "title": "Tests failed on 9988aa", "labels": ["nightly"], }, - # project/repo doesn't have a custom container name file - "container": None, + "command-subject": None, "secrets": ["github-token", "image-download"], "env": { "COCKPIT_BOTS_REF": "main", @@ -345,7 +342,7 @@ def test_amqp_sha_nightly(self, mock_queue, _mock_strftime): "TEST_SCENARIO": "nightly", } } - }) + } # mock time for predictable test name @unittest.mock.patch("time.strftime", return_value="20240102-030405") @@ -375,7 +372,7 @@ def test_amqp_sha_pr(self, mock_queue, _mock_strftime): './make-checkout --verbose --repo=project/repo --rebase=stable-1.0 abcdef && ' 'cd make-checkout-workdir && TEST_OS=fedora BASE_BRANCH=stable-1.0 COCKPIT_BOTS_REF=main ' 'TEST_SCENARIO=nightly ../tests-invoke --pull-number 1 --revision abcdef --repo project/repo"') - self.assertEqual(request, { + assert request == { "command": expected_command, "type": "test", "sha": "abcdef", @@ -385,10 +382,10 @@ def test_amqp_sha_pr(self, mock_queue, _mock_strftime): "context": "fedora/nightly", "repo": "project/repo", "pull": int(self.pull_number), + "report": None, "sha": "abcdef", "slug": f"pull-{self.pull_number}-abcdef-20240102-030405-fedora-nightly", - "target": "stable-1.0", - "container": "supertasks", + "command-subject": None, "secrets": ["github-token", "image-download"], "env": { "BASE_BRANCH": "stable-1.0", @@ -397,7 +394,77 @@ def test_amqp_sha_pr(self, mock_queue, _mock_strftime): "TEST_SCENARIO": "nightly", } } - }) + } + + # mock time for predictable test name + @unittest.mock.patch("time.strftime", return_value="20240102-030405") + @unittest.mock.patch("task.distributed_queue.DistributedQueue") + def do_test_amqp_pr_cross_project(self, status_branch, mock_queue, _mock_strftime): + repo_branch = f"cockpit-project/cockpituous{f'/{status_branch}' if status_branch else ''}" + # SHA is attached to PR #1 + args = ["tests-scan", "--dry", "--repo", self.repo, + # need to pick a project with a REPO_BRANCH_CONTEXT entry for default branch + "--context", f"{self.context}@{repo_branch}", + "--sha", "abcdef", "--amqp", "amqp.example.com:1234"] + + with unittest.mock.patch("sys.argv", args): + # needs to be in-process for mocking + self.get_tests_scan_module().main() + + mock_queue.assert_called_once_with("amqp.example.com:1234", ["rhel", "public"]) + channel = mock_queue.return_value.__enter__.return_value.channel + + channel.basic_publish.assert_called_once() + self.assertEqual(channel.basic_publish.call_args[0][0], "") + self.assertEqual(channel.basic_publish.call_args[0][1], "public") + request = json.loads(channel.basic_publish.call_args[0][2]) + + branch = status_branch or "main" + + # make-checkout tests cockpituous, but tests-invoke *reports* for project/repo + expected_command = ( + './s3-streamer --repo project/repo --test-name pull-1-20240102-030405 ' + f'--github-context fedora/nightly@{repo_branch} --revision abcdef -- ' + '/bin/sh -c "PRIORITY=0005 ' + f'./make-checkout --verbose --repo=cockpit-project/cockpituous --rebase={branch} {branch} && ' + f'cd make-checkout-workdir && TEST_OS=fedora BASE_BRANCH={branch} COCKPIT_BOTS_REF=main ' + 'TEST_SCENARIO=nightly ../tests-invoke --pull-number 1 --revision abcdef --repo project/repo"') + + slug_repo_branch = repo_branch.replace('@', '-').replace('/', '-') + + assert request == { + "command": expected_command, + "type": "test", + "sha": "abcdef", + "ref": branch, + "name": f"pull-{self.pull_number}", + "job": { + "context": f"fedora/nightly@{repo_branch}", + "repo": "project/repo", + "pull": int(self.pull_number), + "report": None, + "sha": "abcdef", + "slug": f"pull-{self.pull_number}-abcdef-20240102-030405-fedora-nightly-{slug_repo_branch}", + "command-subject": {"repo": "cockpit-project/cockpituous", "branch": branch}, + "secrets": ["github-token", "image-download"], + "env": { + "BASE_BRANCH": branch, + "COCKPIT_BOTS_REF": "main", + "TEST_OS": "fedora", + "TEST_SCENARIO": "nightly", + } + } + } + + def test_amqp_sha_pr_cross_project_default_branch(self): + """Default branch cross-project status event on PR""" + + self.do_test_amqp_pr_cross_project(None) + + def test_amqp_sha_pr_cross_project_explicit_branch(self): + """Explicit branch cross-project status event on PR""" + + self.do_test_amqp_pr_cross_project("otherbranch") def do_test_tests_invoke(self, attachments_url, expected_logs_url): repo = "cockpit-project/cockpit" diff --git a/tests-scan b/tests-scan index 52850a6cc6..2205e0b9fe 100755 --- a/tests-scan +++ b/tests-scan @@ -119,7 +119,6 @@ class Job(NamedTuple): repo: str bots_ref: 'str | None' github_context: str - container: 'str | None' # Prepare a human readable output @@ -205,7 +204,8 @@ def queue_test(job: Job, channel, options: argparse.Namespace) -> None: if command: priority = min(job.priority, distributed_queue.MAX_PRIORITY) - slug = f"{job.name}-{job.revision[:8]}-{time.strftime('%Y%m%d-%H%M%S')}-{job.context.replace('/', '-')}" + slug_suffix = job.github_context.replace('/', '-').replace('@', '-') + slug = f"{job.name}-{job.revision[:8]}-{time.strftime('%Y%m%d-%H%M%S')}-{slug_suffix}" (image, _, scenario) = job.context.partition("/") env = { @@ -225,32 +225,26 @@ def queue_test(job: Job, channel, options: argparse.Namespace) -> None: "sha": job.revision, "ref": job.ref, "name": job.name, - - # job-runner doesn't support this yet... - "job": None if job.repo != options.repo else { - "repo": job.repo, + "job": { + "repo": options.repo, "sha": job.revision, - "context": job.context, - "target": job.base, + "context": job.github_context, + "pull": job.number or None, # job.number uses 0 to mean 'None' + "report": None if job.number else { + "title": f"Tests failed on {job.revision}", + "labels": ["nightly"], + }, + "command-subject": None if job.repo == options.repo else { + "repo": job.repo, + "branch": job.ref, + }, "slug": slug, "env": env, - "container": job.container, # for updating naughty trackers and downloading private images "secrets": ["github-token", "image-download"], } } - # report issue for nightly runs - if isinstance(body['job'], dict): - if body["job"] is not None: - if job.number == 0: - body["job"]["report"] = { - "title": f"Tests failed on {job.revision}", - "labels": ["nightly"], - } - else: - body["job"]["pull"] = job.number - queue = 'rhel' if is_internal_context(job.context) else 'public' channel.basic_publish('', queue, json.dumps(body), properties=pika.BasicProperties(priority=priority)) logging.info("Published '%s' on '%s' with command: '%s'", job.name, job.revision, command) @@ -371,7 +365,6 @@ def cockpit_tasks(api: github.GitHub, contexts, opts: argparse.Namespace, repo: statuses = api.statuses(revision) login = pull["head"]["user"]["login"] base = pull.get("base", {}).get("ref") # The branch this pull request targets (None for direct SHA triggers) - merge_sha = pull.get("merge_commit_sha", revision) allowed = login in ALLOWLIST @@ -397,8 +390,6 @@ def cockpit_tasks(api: github.GitHub, contexts, opts: argparse.Namespace, repo: for context in build_policy(repo, contexts).get(base, []): todos[context] = {} - container = api.fetch_file(merge_sha, '.cockpit-ci/container') - # there are 3 different HEADs # ref: the PR or SHA that we are testing # base: the target branch of that PR (None for direct SHA trigger) @@ -434,6 +425,7 @@ def cockpit_tasks(api: github.GitHub, contexts, opts: argparse.Namespace, repo: checkout_ref = ref if project != repo: checkout_ref = testmap.get_default_branch(project) + if base != branch: checkout_ref = branch @@ -460,7 +452,6 @@ def cockpit_tasks(api: github.GitHub, contexts, opts: argparse.Namespace, repo: project, bots_ref, context, - container, )