Repository: packit/packit
Issue: Specfile required in the config (test-only scenario)

This is a regression introduced recently. For my `.packit.yaml` file
```
jobs:
- job: tests
  trigger: pull_request
  metadata:
    targets:
    - fedora-all
    - centos-stream-8-x86_64
    - centos-stream-9-x86_64
    skip_build: true
```
I am getting an error:
```
Failed to load packit config file:
Cannot parse package config: ValidationError({'jobs': {0: {'keylime-tests': ["'specfile_path' is not specified or no specfile was found in the repo"]}}}).
```

Changed files:
packit/config/job_config.py, tests/unit/test_prepare_sources.py, packit/schema.py, tests/unit/test_package_config.py

diff --git a/packit/config/job_config.py b/packit/config/job_config.py
index 628ef216..29645e29 100644
--- a/packit/config/job_config.py
+++ b/packit/config/job_config.py
@@ -64,6 +64,7 @@ class JobConfig(MultiplePackages):
         type: JobType,
         trigger: JobConfigTriggerType,
         packages: Dict[str, CommonPackageConfig],
+        skip_build: bool = False,
     ):
         super().__init__(packages)
         # Directly manipulating __dict__ is not recommended.
@@ -72,6 +73,7 @@ class JobConfig(MultiplePackages):
         # dropped from config objects.
         self.__dict__["type"] = type
         self.__dict__["trigger"] = trigger
+        self.__dict__["skip_build"] = skip_build
 
     def __repr__(self):
         # required to avoid cyclical imports
diff --git a/packit/schema.py b/packit/schema.py
index b82f9053..13faf4ef 100644
--- a/packit/schema.py
+++ b/packit/schema.py
@@ -345,7 +345,6 @@ class CommonConfigSchema(Schema):
     additional_repos = fields.List(fields.String(), missing=None)
     fmf_url = fields.String(missing=None)
     fmf_ref = fields.String(missing=None)
-    skip_build = fields.Boolean()
     env = fields.Dict(keys=fields.String(), missing=None)
     enable_net = fields.Boolean(missing=True)
     allowed_pr_authors = fields.List(fields.String(), missing=None)
@@ -421,6 +420,7 @@ class JobConfigSchema(Schema):
 
     job = EnumField(JobType, required=True, attribute="type")
     trigger = EnumField(JobConfigTriggerType, required=True)
+    skip_build = fields.Boolean()
     packages = fields.Dict(
         keys=fields.String(), values=fields.Nested(CommonConfigSchema())
     )
@@ -449,14 +449,17 @@ class JobConfigSchema(Schema):
             ValidationError, if 'specfile_path' is not specified when
             it should be.
         """
-        # This is somewhat weird: while 'data' is still a dict,
-        # elements in 'jobs' was already loaded, so those are JobConfig objects.
-        if (data["type"] == JobType.tests.value and data.get("skip_build")) or data.get(
+        # Note: At this point, 'data' is still a dict, but values are already
+        # loaded, this is why 'data["type"]' is already a JobType and not a string,
+        # and the package configs below are PackageConfig objects, not dictionaries.
+        if (data["type"] == JobType.tests and data.get("skip_build")) or data.get(
             "specfile_path"
         ):
             return
 
         errors = {}
+        package: str
+        config: PackageConfig
         for package, config in data.get("packages", {}).items():
             if not config.specfile_path:
                 errors[package] = [
@@ -600,8 +603,6 @@ class PackageConfigSchema(Schema):
         jobs = data["jobs"]
         errors = {}
         for i, job in enumerate(jobs):
-            job_type = job.pop("job")
-            trigger = job.pop("trigger")
             # Validate the 'metadata' field if there is any, and merge its
             # content with the job.
             # Do this here in order to avoid complications further in the
@@ -622,7 +623,12 @@ class PackageConfigSchema(Schema):
                     )
                 job.update(metadata)
 
-            selected_packages = job.pop("packages", None)
+            top_keys = {}
+            for key in JobConfigSchema().fields:
+                if (value := job.pop(key, None)) is not None:
+                    top_keys[key] = value
+
+            selected_packages = top_keys.pop("packages", None)
             # Check that only packages which are defined on the top-level are selected.
             # Do this here b/c the code further down requires this to be correct.
             incorrect_packages = (
@@ -639,17 +645,13 @@ class PackageConfigSchema(Schema):
             # There is no 'packages' key in the job, so
             # the job should handle all the top-level packages.
             if not selected_packages:
-                jobs[i] = {
-                    "job": job_type,
-                    "trigger": trigger,
+                jobs[i] = top_keys | {
                     "packages": {k: v | job for k, v in packages.items()},
                 }
             # Some top-level packages are selected to be
             # handled by the job.
             elif isinstance(selected_packages, list):
-                jobs[i] = {
-                    "job": job_type,
-                    "trigger": trigger,
+                jobs[i] = top_keys | {
                     "packages": {
                         k: v | job
                         for k, v in packages.items()
@@ -659,9 +661,7 @@ class PackageConfigSchema(Schema):
             # Some top-level packages are selected to be
             # handled by the job AND have some custom config.
             elif isinstance(selected_packages, dict):
-                jobs[i] = {
-                    "job": job_type,
-                    "trigger": trigger,
+                jobs[i] = top_keys | {
                     "packages": {
                         k: packages[k] | job | v for k, v in selected_packages.items()
                     },
diff --git a/tests/unit/test_package_config.py b/tests/unit/test_package_config.py
index d461fdfd..7b7d00c0 100644
--- a/tests/unit/test_package_config.py
+++ b/tests/unit/test_package_config.py
@@ -1843,7 +1843,6 @@ def test_package_config_specfile_not_present_raise(raw):
     "raw",
     [
         {
-            "downstream_package_name": "package",
             "jobs": [
                 {
                     "job": "tests",
@@ -1854,7 +1853,6 @@ def test_package_config_specfile_not_present_raise(raw):
             ],
         },
         {
-            "downstream_package_name": "package",
             "jobs": [
                 {
                     "job": "tests",
@@ -1865,15 +1863,23 @@ def test_package_config_specfile_not_present_raise(raw):
                 {
                     "job": "tests",
                     "trigger": "pull_request",
-                    "targets": ["fedora-stable", "fedora-development"],
-                    "skip_build": True,
+                    "metadata": {
+                        "targets": ["fedora-stable", "fedora-development"],
+                        "skip_build": True,
+                    },
                 },
             ],
         },
     ],
 )
-def test_package_config_specilfe_not_present_not_raise(raw):
-    assert PackageConfig.get_from_dict(raw_dict=raw)
+def test_specfile_path_not_defined_in_test_only_jobs(raw):
+    """Packages in test jobs, which are configured to not to require building
+    a package don't need a 'specfile_path' set.
+
+    Note: don't set 'downstream_package_name' either, as that implicitly sets
+    'specfile_path' :-)
+    """
+    assert PackageConfig.get_from_dict(raw_dict=raw, repo_name="package")
 
 
 @pytest.mark.parametrize(
diff --git a/tests/unit/test_prepare_sources.py b/tests/unit/test_prepare_sources.py
index da5dd486..e5fdcea4 100644
--- a/tests/unit/test_prepare_sources.py
+++ b/tests/unit/test_prepare_sources.py
@@ -15,6 +15,7 @@ from packit.config import CommonPackageConfig, JobConfig, JobType, JobConfigTrig
             {
                 "job": "copr_build",
                 "trigger": "release",
+                "skip_build": false,
                 "packages": {
                     "package": {
                         "upstream_package_name": null,
@@ -38,7 +39,6 @@ from packit.config import CommonPackageConfig, JobConfig, JobType, JobConfigTrig
                         "additional_repos": [],
                         "env": {},
                         "additional_packages": [],
-                        "skip_build": false,
                         "scratch": false,
                         "targets": {},
                         "fmf_url": null,