Repository: osbuild/osbuild-composer
Pull request #2314 (2022-02-16 12:46:56)

Deduplicate some code in distro definitions
This patch set deduplicates all code in `stage_inputs.go` of each distro definition and a few additional functions, which would have to be otherwise copied again to `rhel-84` due to `gce` image type.

This pull request includes:

- [ ] adequate testing for the new functionality or fixed issue
- [ ] adequate documentation informing people about the change such as
  - [ ] submit a PR for the [guides](https://github.com/osbuild/guides) repository if this PR changed any behavior described there: https://www.osbuild.org/guides/

<!--
Thanks for proposing a change to osbuild-composer!

Please don't remove the above check list. These are things that each pull
request must have before it is merged. It helps maintainers to not forget
anything.

If the reason for ticking any of the boxes is ambiguous, please add a short
note explaining why.

In addition, if this pull request fixes a downstream issue, please refer to
test/README.md and add these additional items:

- [ ] 1st commit of any `rhbz#` related PR contains bug reproducer; CI reports FAIL or
- [ ] PR contains automated tests for new functionality and
- [ ] QE has approved reproducer/new tests and
- [ ] Subsequent commits provide bug fixes without modifying the reproducer; CI reports PASS and
- [ ] QE approves this PR; RHBZ status is set to `MODIFIED + Verified=Tested`
-->



Patch:
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 19335e885..1edd443c5 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -18,6 +18,8 @@ stages:
     paths:
       - journal-log.gpg
       - ci-artifacts
+      - "*.repo"
+      - COMPOSER_NVR
     when: always
 
 .terraform:
@@ -57,10 +59,6 @@ RPM:
   after_script:
     - schutzbot/update_github_status.sh update
     - schutzbot/save_journal.sh
-  artifacts:
-    paths:
-      - journal-log.gpg
-    when: always
   parallel:
     matrix:
       - RUNNER:
@@ -86,9 +84,6 @@ Build -tests RPM for RHEL:
   after_script:
     - schutzbot/update_github_status.sh update
     - schutzbot/save_journal.sh
-  artifacts:
-    paths:
-      - journal-log.gpg
   parallel:
     matrix:
       - <<: *RPM_RUNNERS_RHEL
@@ -165,11 +160,6 @@ Base:
           - aws/rhel-9.0-nightly-x86_64
           - aws/rhel-9.0-nightly-aarch64
         INTERNAL_NETWORK: ["true"]
-  artifacts:
-    paths:
-      - journal-log.gpg
-      - "*.repo"
-    when: always
 
 Manifests:
   stage: test
@@ -186,11 +176,6 @@ Manifests:
       - RUNNER:
           - aws/rhel-9.0-nightly-x86_64
         INTERNAL_NETWORK: ["true"]
-  artifacts:
-    paths:
-      - journal-log.gpg
-      - "*.repo"
-    when: always
 
 Regression:
   stage: test
@@ -219,12 +204,6 @@ Regression:
           - aws/rhel-9.0-nightly-x86_64
           - aws/rhel-9.0-nightly-aarch64
         INTERNAL_NETWORK: ["true"]
-  artifacts:
-    paths:
-      - journal-log.gpg
-      - "*.repo"
-    when: always
-
 
 Image Tests:
   stage: test
@@ -244,12 +223,6 @@ Image Tests:
           - aws/rhel-9.0-nightly-x86_64
           - aws/rhel-9.0-nightly-aarch64
         INTERNAL_NETWORK: ["true"]
-  artifacts:
-    paths:
-      - journal-log.gpg
-      - "*.repo"
-    when: always
-
 
 Test Case Generation:
   stage: test
@@ -264,11 +237,6 @@ Test Case Generation:
       - RUNNER:
           - aws/fedora-34-x86_64
         INTERNAL_NETWORK: ["true"]
-  artifacts:
-    paths:
-      - journal-log.gpg
-      - "*.repo"
-    when: always
 
 OSTree:
   stage: test
@@ -439,6 +407,8 @@ Upgrade:
       - ci-artifacts
       - install_console.log
       - upgrade_console.log
+      - leapp-preupgrade.log
+      - leapp-report.txt
       - osbuild-upgrade8to9.log
       - osbuild-upgrade8to9.json
     when: always
diff --git a/cmd/osbuild-worker/jobimpl-koji-finalize.go b/cmd/osbuild-worker/jobimpl-koji-finalize.go
index 8d4780834..7a21a5892 100644
--- a/cmd/osbuild-worker/jobimpl-koji-finalize.go
+++ b/cmd/osbuild-worker/jobimpl-koji-finalize.go
@@ -7,6 +7,7 @@ import (
 
 	"github.com/sirupsen/logrus"
 
+	osbuild "github.com/osbuild/osbuild-composer/internal/osbuild2"
 	"github.com/osbuild/osbuild-composer/internal/rpmmd"
 	"github.com/osbuild/osbuild-composer/internal/upload/koji"
 	"github.com/osbuild/osbuild-composer/internal/worker"
@@ -127,7 +128,7 @@ func (impl *KojiFinalizeJobImpl) Run(job worker.Job) error {
 		// collect packages from stages in build pipelines
 		for _, plName := range buildArgs.PipelineNames.Build {
 			buildPipelineMd := buildArgs.OSBuildOutput.Metadata[plName]
-			buildRPMs = append(buildRPMs, rpmmd.OSBuildMetadataToRPMs(buildPipelineMd)...)
+			buildRPMs = append(buildRPMs, osbuild.OSBuildMetadataToRPMs(buildPipelineMd)...)
 		}
 		// this dedupe is usually not necessary since we generally only have
 		// one rpm stage in one build pipeline, but it's not invalid to have
@@ -155,7 +156,7 @@ func (impl *KojiFinalizeJobImpl) Run(job worker.Job) error {
 		imageRPMs := make([]rpmmd.RPM, 0)
 		for _, plName := range buildArgs.PipelineNames.Payload {
 			payloadPipelineMd := buildArgs.OSBuildOutput.Metadata[plName]
-			imageRPMs = append(imageRPMs, rpmmd.OSBuildMetadataToRPMs(payloadPipelineMd)...)
+			imageRPMs = append(imageRPMs, osbuild.OSBuildMetadataToRPMs(payloadPipelineMd)...)
 		}
 
 		// deduplicate
diff --git a/internal/cloudapi/v2/v2.go b/internal/cloudapi/v2/v2.go
index b3732c9ab..ddfe7d28e 100644
--- a/internal/cloudapi/v2/v2.go
+++ b/internal/cloudapi/v2/v2.go
@@ -1013,7 +1013,7 @@ func stagesToPackageMetadata(stages []osbuild.RPMStageMetadata) []PackageMetadat
 					Epoch:     rpm.Epoch,
 					Arch:      rpm.Arch,
 					Sigmd5:    rpm.SigMD5,
-					Signature: rpmmd.PackageMetadataToSignature(rpm),
+					Signature: osbuild.RPMPackageMetadataToSignature(rpm),
 				},
 			)
 		}
diff --git a/internal/distro/rhel84/distro_v2.go b/internal/distro/rhel84/distro_v2.go
index 0e27e6ff2..091d8f44b 100644
--- a/internal/distro/rhel84/distro_v2.go
+++ b/internal/distro/rhel84/distro_v2.go
@@ -275,7 +275,7 @@ func (t *imageTypeS2) buildPipeline(repos []rpmmd.RepoConfig, buildPackageSpecs
 	p := new(osbuild.Pipeline)
 	p.Name = "build"
 	p.Runner = "org.osbuild.rhel84"
-	p.AddStage(osbuild.NewRPMStage(t.rpmStageOptions(repos), t.rpmStageInputs(buildPackageSpecs)))
+	p.AddStage(osbuild.NewRPMStage(t.rpmStageOptions(repos), osbuild.NewRpmStageSourceFilesInputs(buildPackageSpecs)))
 	p.AddStage(osbuild.NewSELinuxStage(t.selinuxStageOptions()))
 	return p
 }
@@ -284,7 +284,7 @@ func (t *imageTypeS2) ostreeTreePipeline(repos []rpmmd.RepoConfig, packages []rp
 	p := new(osbuild.Pipeline)
 	p.Name = "ostree-tree"
 	p.Build = "name:build"
-	p.AddStage(osbuild.NewRPMStage(t.rpmStageOptions(repos), t.rpmStageInputs(packages)))
+	p.AddStage(osbuild.NewRPMStage(t.rpmStageOptions(repos), osbuild.NewRpmStageSourceFilesInputs(packages)))
 	language, keyboard := c.GetPrimaryLocale()
 	if language != nil {
 		p.AddStage(osbuild.NewLocaleStage(&osbuild.LocaleStageOptions{Language: *language}))
@@ -381,7 +381,7 @@ func (t *imageTypeS2) containerTreePipeline(repos []rpmmd.RepoConfig, packages [
 	p := new(osbuild.Pipeline)
 	p.Name = "container-tree"
 	p.Build = "name:build"
-	p.AddStage(osbuild.NewRPMStage(t.rpmStageOptions(repos), t.rpmStageInputs(packages)))
+	p.AddStage(osbuild.NewRPMStage(t.rpmStageOptions(repos), osbuild.NewRpmStageSourceFilesInputs(packages)))
 	language, _ := c.GetPrimaryLocale()
 	if language != nil {
 		p.AddStage(osbuild.NewLocaleStage(&osbuild.LocaleStageOptions{Language: *language}))
@@ -392,7 +392,7 @@ func (t *imageTypeS2) containerTreePipeline(repos []rpmmd.RepoConfig, packages [
 
 	p.AddStage(osbuild.NewOSTreePullStage(
 		&osbuild.OSTreePullStageOptions{Repo: "/var/www/html/repo"},
-		t.ostreePullStageInputs("org.osbuild.pipeline", "name:ostree-commit", options.OSTree.Ref),
+		osbuild.NewOstreePullStageInputs("org.osbuild.pipeline", "name:ostree-commit", options.OSTree.Ref),
 	))
 	return p
 }
@@ -424,11 +424,11 @@ func (t *imageTypeS2) anacondaTreePipeline(repos []rpmmd.RepoConfig, packages []
 	p := new(osbuild.Pipeline)
 	p.Name = "anaconda-tree"
 	p.Build = "name:build"
-	p.AddStage(osbuild.NewRPMStage(t.rpmStageOptions(repos), t.rpmStageInputs(packages)))
+	p.AddStage(osbuild.NewRPMStage(t.rpmStageOptions(repos), osbuild.NewRpmStageSourceFilesInputs(packages)))
 	p.AddStage(osbuild.NewOSTreeInitStage(&osbuild.OSTreeInitStageOptions{Path: ostreeRepoPath}))
 	p.AddStage(osbuild.NewOSTreePullStage(
 		&osbuild.OSTreePullStageOptions{Repo: ostreeRepoPath},
-		t.ostreePullStageInputs("org.osbuild.source", options.OSTree.Parent, options.OSTree.Ref),
+		osbuild.NewOstreePullStageInputs("org.osbuild.source", options.OSTree.Parent, options.OSTree.Ref),
 	))
 	p.AddStage(osbuild.NewBuildstampStage(t.buildStampStageOptions()))
 	p.AddStage(osbuild.NewLocaleStage(&osbuild.LocaleStageOptions{Language: "en_US.UTF-8"}))
@@ -471,7 +471,7 @@ func (t *imageTypeS2) bootISOTreePipeline(kernelVer string) *osbuild.Pipeline {
 	p.Name = "bootiso-tree"
 	p.Build = "name:build"
 
-	p.AddStage(osbuild.NewBootISOMonoStage(t.bootISOMonoStageOptions(kernelVer), t.bootISOMonoStageInputs()))
+	p.AddStage(osbuild.NewBootISOMonoStage(t.bootISOMonoStageOptions(kernelVer), osbuild.NewBootISOMonoStagePipelineTreeInputs("anaconda-tree")))
 	p.AddStage(osbuild.NewDiscinfoStage(t.discinfoStageOptions()))
 
 	return p
@@ -482,39 +482,12 @@ func (t *imageTypeS2) bootISOPipeline() *osbuild.Pipeline {
 	p.Name = "assembler"
 	p.Build = "name:build"
 
-	p.AddStage(osbuild.NewXorrisofsStage(t.xorrisofsStageOptions(), t.xorrisofsStageInputs()))
+	p.AddStage(osbuild.NewXorrisofsStage(t.xorrisofsStageOptions(), osbuild.NewXorrisofsStagePipelineTreeInputs("bootiso-tree")))
 	p.AddStage(osbuild.NewImplantisomd5Stage(&osbuild.Implantisomd5StageOptions{Filename: t.Filename()}))
 
 	return p
 }
 
-func (t *imageTypeS2) rpmStageInputs(specs []rpmmd.PackageSpec) *osbuild.RPMStageInputs {
-	stageInput := new(osbuild.RPMStageInput)
-	stageInput.Type = "org.osbuild.files"
-	stageInput.Origin = "org.osbuild.source"
-	stageInput.References = pkgRefs(specs)
-	return &osbuild.RPMStageInputs{Packages: stageInput}
-}
-
-func pkgRefs(specs []rpmmd.PackageSpec) osbuild.RPMStageReferences {
-	refs := make([]string, len(specs))
-	for idx, pkg := range specs {
-		refs[idx] = pkg.Checksum
-	}
-	return refs
-}
-
-func (t *imageTypeS2) ostreePullStageInputs(origin, source, commitRef string) *osbuild.OSTreePullStageInputs {
-	pullStageInput := new(osbuild.OSTreePullStageInput)
-	pullStageInput.Type = "org.osbuild.ostree"
-	pullStageInput.Origin = origin
-
-	inputRefs := make(map[string]osbuild.OSTreePullStageReference)
-	inputRefs[source] = osbuild.OSTreePullStageReference{Ref: commitRef}
-	pullStageInput.References = inputRefs
-	return &osbuild.OSTreePullStageInputs{Commits: pullStageInput}
-}
-
 func (t *imageTypeS2) rpmStageOptions(repos []rpmmd.RepoConfig) *osbuild.RPMStageOptions {
 	var gpgKeys []string
 	for _, repo := range repos {
@@ -775,16 +748,6 @@ func (t *imageTypeS2) bootISOMonoStageOptions(kernelVer string) *osbuild.BootISO
 	}
 }
 
-func (t *imageTypeS2) bootISOMonoStageInputs() *osbuild.BootISOMonoStageInputs {
-	rootfsInput := new(osbuild.BootISOMonoStageInput)
-	rootfsInput.Type = "org.osbuild.tree"
-	rootfsInput.Origin = "org.osbuild.pipeline"
-	rootfsInput.References = osbuild.BootISOMonoStageReferences{"name:anaconda-tree"}
-	return &osbuild.BootISOMonoStageInputs{
-		RootFS: rootfsInput,
-	}
-}
-
 func (t *imageTypeS2) discinfoStageOptions() *osbuild.DiscinfoStageOptions {
 	return &osbuild.DiscinfoStageOptions{
 		BaseArch: t.Arch().Name(),
@@ -805,11 +768,3 @@ func (t *imageTypeS2) xorrisofsStageOptions() *osbuild.XorrisofsStageOptions {
 		IsohybridMBR: "/usr/share/syslinux/isohdpfx.bin",
 	}
 }
-
-func (t *imageTypeS2) xorrisofsStageInputs() *osbuild.XorrisofsStageInputs {
-	input := new(osbuild.XorrisofsStageInput)
-	input.Type = "org.osbuild.tree"
-	input.Origin = "org.osbuild.pipeline"
-	input.References = osbuild.XorrisofsStageReferences{"name:bootiso-tree"}
-	return &osbuild.XorrisofsStageInputs{Tree: input}
-}
diff --git a/internal/distro/rhel85/pipelines.go b/internal/distro/rhel85/pipelines.go
index 75a74d0ff..05c778827 100644
--- a/internal/distro/rhel85/pipelines.go
+++ b/internal/distro/rhel85/pipelines.go
@@ -47,7 +47,7 @@ func qcow2Pipelines(t *imageType, customizations *blueprint.Customizations, opti
 	}
 
 	treePipeline.AddStage(osbuild.NewFSTabStage(partitionTable.FSTabStageOptionsV2()))
-	kernelVer := kernelVerStr(packageSetSpecs[blueprintPkgsKey], customizations.GetKernel().Name, t.Arch().Name())
+	kernelVer := rpmmd.GetVerStrFromPackageSpecListPanic(packageSetSpecs[blueprintPkgsKey], customizations.GetKernel().Name)
 	treePipeline.AddStage(bootloaderConfigStage(t, partitionTable, customizations.GetKernel(), kernelVer, false, false))
 	treePipeline.AddStage(osbuild.NewSELinuxStage(selinuxStageOptions(false)))
 	pipelines = append(pipelines, *treePipeline)
@@ -68,7 +68,7 @@ func prependKernelCmdlineStage(pipeline *osbuild.Pipeline, t *imageType, pt *dis
 		if rootPartition == nil {
 			panic("s390x image must have a root partition, this is a programming error")
 		}
-		kernelStage := osbuild.NewKernelCmdlineStage(kernelCmdlineStageOptions(rootPartition.Filesystem.UUID, t.kernelOptions))
+		kernelStage := osbuild.NewKernelCmdlineStage(osbuild.NewKernelCmdlineStageOptions(rootPartition.Filesystem.UUID, t.kernelOptions))
 		pipeline.Stages = append([]*osbuild.Stage{kernelStage}, pipeline.Stages...)
 	}
 	return pipeline
@@ -88,7 +88,7 @@ func vhdPipelines(t *imageType, customizations *blueprint.Customizations, option
 	}
 
 	treePipeline.AddStage(osbuild.NewFSTabStage(partitionTable.FSTabStageOptionsV2()))
-	kernelVer := kernelVerStr(packageSetSpecs[blueprintPkgsKey], customizations.GetKernel().Name, t.Arch().Name())
+	kernelVer := rpmmd.GetVerStrFromPackageSpecListPanic(packageSetSpecs[blueprintPkgsKey], customizations.GetKernel().Name)
 	treePipeline.AddStage(bootloaderConfigStage(t, partitionTable, customizations.GetKernel(), kernelVer, false, false))
 	treePipeline.AddStage(osbuild.NewSELinuxStage(selinuxStageOptions(false)))
 	pipelines = append(pipelines, *treePipeline)
@@ -119,7 +119,7 @@ func vmdkPipelines(t *imageType, customizations *blueprint.Customizations, optio
 	}
 
 	treePipeline.AddStage(osbuild.NewFSTabStage(partitionTable.FSTabStageOptionsV2()))
-	kernelVer := kernelVerStr(packageSetSpecs[blueprintPkgsKey], customizations.GetKernel().Name, t.Arch().Name())
+	kernelVer := rpmmd.GetVerStrFromPackageSpecListPanic(packageSetSpecs[blueprintPkgsKey], customizations.GetKernel().Name)
 	treePipeline.AddStage(bootloaderConfigStage(t, partitionTable, customizations.GetKernel(), kernelVer, false, false))
 	treePipeline.AddStage(osbuild.NewSELinuxStage(selinuxStageOptions(false)))
 	pipelines = append(pipelines, *treePipeline)
@@ -150,7 +150,7 @@ func openstackPipelines(t *imageType, customizations *blueprint.Customizations,
 	}
 
 	treePipeline.AddStage(osbuild.NewFSTabStage(partitionTable.FSTabStageOptionsV2()))
-	kernelVer := kernelVerStr(packageSetSpecs[blueprintPkgsKey], customizations.GetKernel().Name, t.Arch().Name())
+	kernelVer := rpmmd.GetVerStrFromPackageSpecListPanic(packageSetSpecs[blueprintPkgsKey], customizations.GetKernel().Name)
 	treePipeline.AddStage(bootloaderConfigStage(t, partitionTable, customizations.GetKernel(), kernelVer, false, false))
 	treePipeline.AddStage(osbuild.NewSELinuxStage(selinuxStageOptions(false)))
 	pipelines = append(pipelines, *treePipeline)
@@ -184,7 +184,7 @@ func ec2BaseTreePipeline(repos []rpmmd.RepoConfig, packages []rpmmd.PackageSpec,
 	p.Name = "os"
 	p.Build = "name:build"
 	packages = append(packages, bpPackages...)
-	p.AddStage(osbuild.NewRPMStage(rpmStageOptions(repos), rpmStageInputs(packages)))
+	p.AddStage(osbuild.NewRPMStage(rpmStageOptions(repos), osbuild.NewRpmStageSourceFilesInputs(packages)))
 
 	// If the /boot is on a separate partition, the prefix for the BLS stage must be ""
 	if pt.BootPartition() == nil {
@@ -432,7 +432,7 @@ func ec2CommonPipelines(t *imageType, customizations *blueprint.Customizations,
 	}
 
 	treePipeline.AddStage(osbuild.NewFSTabStage(partitionTable.FSTabStageOptionsV2()))
-	kernelVer := kernelVerStr(packageSetSpecs[blueprintPkgsKey], customizations.GetKernel().Name, t.Arch().Name())
+	kernelVer := rpmmd.GetVerStrFromPackageSpecListPanic(packageSetSpecs[blueprintPkgsKey], customizations.GetKernel().Name)
 	treePipeline.AddStage(bootloaderConfigStage(t, partitionTable, customizations.GetKernel(), kernelVer, false, false))
 	// The last stage must be the SELinux stage
 	treePipeline.AddStage(osbuild.NewSELinuxStage(selinuxStageOptions(false)))
@@ -493,7 +493,7 @@ func edgeInstallerPipelines(t *imageType, customizations *blueprint.Customizatio
 	pipelines := make([]osbuild.Pipeline, 0)
 	pipelines = append(pipelines, *buildPipeline(repos, packageSetSpecs[buildPkgsKey]))
 	installerPackages := packageSetSpecs[installerPkgsKey]
-	kernelVer := kernelVerStr(installerPackages, "kernel", t.Arch().Name())
+	kernelVer := rpmmd.GetVerStrFromPackageSpecListPanic(installerPackages, "kernel")
 	ostreeRepoPath := "/ostree/repo"
 	payloadStages := ostreePayloadStages(options, ostreeRepoPath)
 	kickstartOptions := ostreeKickstartStageOptions(makeISORootPath(ostreeRepoPath), options.OSTree.Ref)
@@ -626,7 +626,7 @@ func buildPipeline(repos []rpmmd.RepoConfig, buildPackageSpecs []rpmmd.PackageSp
 	p := new(osbuild.Pipeline)
 	p.Name = "build"
 	p.Runner = "org.osbuild.rhel85"
-	p.AddStage(osbuild.NewRPMStage(rpmStageOptions(repos), rpmStageInputs(buildPackageSpecs)))
+	p.AddStage(osbuild.NewRPMStage(rpmStageOptions(repos), osbuild.NewRpmStageSourceFilesInputs(buildPackageSpecs)))
 	p.AddStage(osbuild.NewSELinuxStage(selinuxStageOptions(true)))
 	return p
 }
@@ -636,7 +636,7 @@ func osPipeline(repos []rpmmd.RepoConfig, packages []rpmmd.PackageSpec, bpPackag
 	p.Name = "os"
 	p.Build = "name:build"
 	packages = append(packages, bpPackages...)
-	p.AddStage(osbuild.NewRPMStage(rpmStageOptions(repos), rpmStageInputs(packages)))
+	p.AddStage(osbuild.NewRPMStage(rpmStageOptions(repos), osbuild.NewRpmStageSourceFilesInputs(packages)))
 	p.AddStage(osbuild.NewFixBLSStage(&osbuild.FixBLSStageOptions{}))
 	language, keyboard := c.GetPrimaryLocale()
 	if language != nil {
@@ -722,7 +722,7 @@ func ostreeTreePipeline(repos []rpmmd.RepoConfig, packages []rpmmd.PackageSpec,
 		p.AddStage(osbuild2.NewOSTreePasswdStage("org.osbuild.source", options.OSTree.Parent))
 	}
 
-	p.AddStage(osbuild.NewRPMStage(rpmStageOptions(repos), rpmStageInputs(packages)))
+	p.AddStage(osbuild.NewRPMStage(rpmStageOptions(repos), osbuild.NewRpmStageSourceFilesInputs(packages)))
 	p.AddStage(osbuild.NewFixBLSStage(&osbuild.FixBLSStageOptions{}))
 	language, keyboard := c.GetPrimaryLocale()
 	if language != nil {
@@ -839,7 +839,7 @@ func containerTreePipeline(repos []rpmmd.RepoConfig, packages []rpmmd.PackageSpe
 	p := new(osbuild.Pipeline)
 	p.Name = "container-tree"
 	p.Build = "name:build"
-	p.AddStage(osbuild.NewRPMStage(rpmStageOptions(repos), rpmStageInputs(packages)))
+	p.AddStage(osbuild.NewRPMStage(rpmStageOptions(repos), osbuild.NewRpmStageSourceFilesInputs(packages)))
 	language, _ := c.GetPrimaryLocale()
 	if language != nil {
 		p.AddStage(osbuild.NewLocaleStage(&osbuild.LocaleStageOptions{Language: *language}))
@@ -853,7 +853,7 @@ func containerTreePipeline(repos []rpmmd.RepoConfig, packages []rpmmd.PackageSpe
 
 	p.AddStage(osbuild.NewOSTreePullStage(
 		&osbuild.OSTreePullStageOptions{Repo: repoPath},
-		ostreePullStageInputs("org.osbuild.pipeline", "name:ostree-commit", options.OSTree.Ref),
+		osbuild.NewOstreePullStageInputs("org.osbuild.pipeline", "name:ostree-commit", options.OSTree.Ref),
 	))
 
 	// make nginx log directory world writeable, otherwise nginx can't start in
@@ -892,7 +892,7 @@ func ostreePayloadStages(options distro.ImageOptions, ostreeRepoPath string) []*
 	stages = append(stages, osbuild.NewOSTreeInitStage(&osbuild.OSTreeInitStageOptions{Path: ostreeRepoPath}))
 	stages = append(stages, osbuild.NewOSTreePullStage(
 		&osbuild.OSTreePullStageOptions{Repo: ostreeRepoPath},
-		ostreePullStageInputs("org.osbuild.source", options.OSTree.Parent, options.OSTree.Ref),
+		osbuild.NewOstreePullStageInputs("org.osbuild.source", options.OSTree.Parent, options.OSTree.Ref),
 	))
 
 	return stages
@@ -902,7 +902,7 @@ func edgeSimplifiedInstallerPipelines(t *imageType, customizations *blueprint.Cu
 	pipelines := make([]osbuild.Pipeline, 0)
 	pipelines = append(pipelines, *buildPipeline(repos, packageSetSpecs[buildPkgsKey]))
 	installerPackages := packageSetSpecs[installerPkgsKey]
-	kernelVer := kernelVerStr(installerPackages, "kernel", t.Arch().Name())
+	kernelVer := rpmmd.GetVerStrFromPackageSpecListPanic(installerPackages, "kernel")
 	imgName := "disk.img.xz"
 	installDevice := customizations.GetInstallationDevice()
 
@@ -979,12 +979,12 @@ func simplifiedInstallerBootISOTreePipeline(archivePipelineName, kver string) *o
 	}
 
 	inputName := "root-tree"
-	copyInputs := copyPipelineTreeInputs(inputName, "efiboot-tree")
+	copyInputs := osbuild.NewCopyStagePipelineTreeInputs(inputName, "efiboot-tree")
 	copyOptions, copyDevices, copyMounts := copyFSTreeOptions(inputName, "efiboot-tree", &pt, loopback)
 	p.AddStage(osbuild.NewCopyStage(copyOptions, copyInputs, copyDevices, copyMounts))
 
 	inputName = "coi"
-	copyInputs = copyPipelineTreeInputs(inputName, "coi-tree")
+	copyInputs = osbuild.NewCopyStagePipelineTreeInputs(inputName, "coi-tree")
 	p.AddStage(osbuild.NewCopyStageSimple(
 		&osbuild.CopyStageOptions{
 			Paths: []osbuild.CopyStagePath{
@@ -1002,7 +1002,7 @@ func simplifiedInstallerBootISOTreePipeline(archivePipelineName, kver string) *o
 	))
 
 	inputName = "efi-tree"
-	copyInputs = copyPipelineTreeInputs(inputName, "efiboot-tree")
+	copyInputs = osbuild.NewCopyStagePipelineTreeInputs(inputName, "efiboot-tree")
 	p.AddStage(osbuild.NewCopyStageSimple(
 		&osbuild.CopyStageOptions{
 			Paths: []osbuild.CopyStagePath{
@@ -1066,7 +1066,7 @@ func simplifiedInstallerTreePipeline(repos []rpmmd.RepoConfig, packages []rpmmd.
 	p := new(osbuild.Pipeline)
 	p.Name = "coi-tree"
 	p.Build = "name:build"
-	p.AddStage(osbuild.NewRPMStage(rpmStageOptions(repos), rpmStageInputs(packages)))
+	p.AddStage(osbuild.NewRPMStage(rpmStageOptions(repos), osbuild.NewRpmStageSourceFilesInputs(packages)))
 	p.AddStage(osbuild.NewBuildstampStage(buildStampStageOptions(arch)))
 	p.AddStage(osbuild.NewLocaleStage(&osbuild.LocaleStageOptions{Language: "en_US.UTF-8"}))
 	p.AddStage(osbuild.NewSystemdStage(systemdStageOptions([]string{"coreos-installer"}, nil, nil, "")))
@@ -1095,7 +1095,7 @@ func ostreeDeployPipeline(
 	p.AddStage(osbuild.OSTreeInitFsStage())
 	p.AddStage(osbuild.NewOSTreePullStage(
 		&osbuild.OSTreePullStageOptions{Repo: repoPath},
-		ostreePullStageInputs("org.osbuild.source", options.OSTree.Parent, options.OSTree.Ref),
+		osbuild.NewOstreePullStageInputs("org.osbuild.source", options.OSTree.Parent, options.OSTree.Ref),
 	))
 	p.AddStage(osbuild.NewOSTreeOsInitStage(
 		&osbuild.OSTreeOsInitStageOptions{
@@ -1155,7 +1155,7 @@ func anacondaTreePipeline(repos []rpmmd.RepoConfig, packages []rpmmd.PackageSpec
 	p := new(osbuild.Pipeline)
 	p.Name = "anaconda-tree"
 	p.Build = "name:build"
-	p.AddStage(osbuild.NewRPMStage(rpmStageOptions(repos), rpmStageInputs(packages)))
+	p.AddStage(osbuild.NewRPMStage(rpmStageOptions(repos), osbuild.NewRpmStageSourceFilesInputs(packages)))
 	p.AddStage(osbuild.NewBuildstampStage(buildStampStageOptions(arch)))
 	p.AddStage(osbuild.NewLocaleStage(&osbuild.LocaleStageOptions{Language: "en_US.UTF-8"}))
 
@@ -1198,7 +1198,7 @@ func bootISOTreePipeline(kernelVer string, arch string, ksOptions *osbuild.Kicks
 	p.Name = "bootiso-tree"
 	p.Build = "name:build"
 
-	p.AddStage(osbuild.NewBootISOMonoStage(bootISOMonoStageOptions(kernelVer, arch), bootISOMonoStageInputs()))
+	p.AddStage(osbuild.NewBootISOMonoStage(bootISOMonoStageOptions(kernelVer, arch), osbuild.NewBootISOMonoStagePipelineTreeInputs("anaconda-tree")))
 	p.AddStage(osbuild.NewKickstartStage(ksOptions))
 	p.AddStage(osbuild.NewDiscinfoStage(discinfoStageOptions(arch)))
 
@@ -1213,7 +1213,7 @@ func bootISOPipeline(filename string, arch string, isolinux bool) *osbuild.Pipel
 	p.Name = "bootiso"
 	p.Build = "name:build"
 
-	p.AddStage(osbuild.NewXorrisofsStage(xorrisofsStageOptions(filename, arch, isolinux), xorrisofsStageInputs("bootiso-tree")))
+	p.AddStage(osbuild.NewXorrisofsStage(xorrisofsStageOptions(filename, arch, isolinux), osbuild.NewXorrisofsStagePipelineTreeInputs("bootiso-tree")))
 	p.AddStage(osbuild.NewImplantisomd5Stage(&osbuild.Implantisomd5StageOptions{Filename: filename}))
 
 	return p
@@ -1235,7 +1235,7 @@ func liveImagePipeline(inputPipelineName string, outputFilename string, pt *disk
 
 	inputName := "root-tree"
 	copyOptions, copyDevices, copyMounts := copyFSTreeOptions(inputName, inputPipelineName, pt, loopback)
-	copyInputs := copyPipelineTreeInputs(inputName, inputPipelineName)
+	copyInputs := osbuild.NewCopyStagePipelineTreeInputs(inputName, inputPipelineName)
 	p.AddStage(osbuild.NewCopyStage(copyOptions, copyInputs, copyDevices, copyMounts))
 	p.AddStage(bootloaderInstStage(outputFilename, pt, arch, kernelVer, copyDevices, copyMounts, loopback))
 	return p
@@ -1316,7 +1316,7 @@ func qemuPipeline(inputPipelineName, inputFilename, outputFilename, format, qcow
 	p.Name = format
 	p.Build = "name:build"
 
-	qemuStage := osbuild.NewQEMUStage(qemuStageOptions(outputFilename, format, qcow2Compat), qemuStageInputs(inputPipelineName, inputFilename))
+	qemuStage := osbuild.NewQEMUStage(qemuStageOptions(outputFilename, format, qcow2Compat), osbuild.NewQemuStagePipelineFilesInputs(inputPipelineName, inputFilename))
 	p.AddStage(qemuStage)
 	return p
 }
@@ -1348,19 +1348,3 @@ func bootloaderInstStage(filename string, pt *disk.PartitionTable, arch *archite
 
 	return nil
 }
-
-func kernelVerStr(pkgs []rpmmd.PackageSpec, kernelName, arch string) string {
-	kernelPkg := new(rpmmd.PackageSpec)
-	for _, pkg := range pkgs {
-		if pkg.Name == kernelName {
-			// Implicit memory alasing doesn't couse any bug in this case
-			/* #nosec G601 */
-			kernelPkg = &pkg
-			break
-		}
-	}
-	if kernelPkg == nil {
-		panic(fmt.Sprintf("kernel package %q not found", kernelName))
-	}
-	return fmt.Sprintf("%s-%s.%s", kernelPkg.Version, kernelPkg.Release, kernelPkg.Arch)
-}
diff --git a/internal/distro/rhel85/stage_inputs.go b/internal/distro/rhel85/stage_inputs.go
deleted file mode 100644
index 80bc2690a..000000000
--- a/internal/distro/rhel85/stage_inputs.go
+++ /dev/null
@@ -1,74 +0,0 @@
-package rhel85
-
-import (
-	osbuild "github.com/osbuild/osbuild-composer/internal/osbuild2"
-	"github.com/osbuild/osbuild-composer/internal/rpmmd"
-)
-
-func bootISOMonoStageInputs() *osbuild.BootISOMonoStageInputs {
-	rootfsInput := new(osbuild.BootISOMonoStageInput)
-	rootfsInput.Type = "org.osbuild.tree"
-	rootfsInput.Origin = "org.osbuild.pipeline"
-	rootfsInput.References = osbuild.BootISOMonoStageReferences{"name:anaconda-tree"}
-	return &osbuild.BootISOMonoStageInputs{
-		RootFS: rootfsInput,
-	}
-}
-
-func rpmStageInputs(specs []rpmmd.PackageSpec) *osbuild.RPMStageInputs {
-	stageInput := new(osbuild.RPMStageInput)
-	stageInput.Type = "org.osbuild.files"
-	stageInput.Origin = "org.osbuild.source"
-	stageInput.References = pkgRefs(specs)
-	return &osbuild.RPMStageInputs{Packages: stageInput}
-}
-
-func pkgRefs(specs []rpmmd.PackageSpec) osbuild.RPMStageReferences {
-	refs := make([]string, len(specs))
-	for idx, pkg := range specs {
-		refs[idx] = pkg.Checksum
-	}
-	return refs
-}
-
-func ostreePullStageInputs(origin, source, commitRef string) *osbuild.OSTreePullStageInputs {
-	pullStageInput := new(osbuild.OSTreePullStageInput)
-	pullStageInput.Type = "org.osbuild.ostree"
-	pullStageInput.Origin = origin
-
-	inputRefs := make(map[string]osbuild.OSTreePullStageReference)
-	inputRefs[source] = osbuild.OSTreePullStageReference{Ref: commitRef}
-	pullStageInput.References = inputRefs
-	return &osbuild.OSTreePullStageInputs{Commits: pullStageInput}
-}
-
-func xorrisofsStageInputs(pipeline string) *osbuild.XorrisofsStageInputs {
-	input := new(osbuild.XorrisofsStageInput)
-	input.Type = "org.osbuild.tree"
-	input.Origin = "org.osbuild.pipeline"
-	input.References = osbuild.XorrisofsStageReferences{"name:" + pipeline}
-	return &osbuild.XorrisofsStageInputs{Tree: input}
-}
-
-func copyPipelineTreeInputs(name, inputPipeline string) *osbuild.CopyStageInputs {
-	inputName := name
-	treeInput := osbuild.CopyStageInput{}
-	treeInput.Type = "org.osbuild.tree"
-	treeInput.Origin = "org.osbuild.pipeline"
-	treeInput.References = []string{"name:" + inputPipeline}
-	return &osbuild.CopyStageInputs{inputName: treeInput}
-}
-
-func qemuStageInputs(stage, file string) *osbuild.QEMUStageInputs {
-	stageKey := "name:" + stage
-	ref := map[string]osbuild.QEMUFile{
-		stageKey: {
-			File: file,
-		},
-	}
-	input := new(osbuild.QEMUStageInput)
-	input.Type = "org.osbuild.files"
-	input.Origin = "org.osbuild.pipeline"
-	input.References = ref
-	return &osbuild.QEMUStageInputs{Image: input}
-}
diff --git a/internal/distro/rhel85/stage_options.go b/internal/distro/rhel85/stage_options.go
index 9ae5b64a9..90d876f27 100644
--- a/internal/distro/rhel85/stage_options.go
+++ b/internal/distro/rhel85/stage_options.go
@@ -532,13 +532,6 @@ func qemuStageOptions(filename, format, compat string) *osbuild.QEMUStageOptions
 	}
 }
 
-func kernelCmdlineStageOptions(rootUUID string, kernelOptions string) *osbuild.KernelCmdlineStageOptions {
-	return &osbuild.KernelCmdlineStageOptions{
-		RootFsUUID: rootUUID,
-		KernelOpts: kernelOptions,
-	}
-}
-
 func nginxConfigStageOptions(path, htmlRoot, listen string) *osbuild.NginxConfigStageOptions {
 	// configure nginx to work in an unprivileged container
 	cfg := &osbuild.NginxConfig{
diff --git a/internal/distro/rhel86/pipelines.go b/internal/distro/rhel86/pipelines.go
index 4e1d87908..02521de8a 100644
--- a/internal/distro/rhel86/pipelines.go
+++ b/internal/distro/rhel86/pipelines.go
@@ -31,7 +31,7 @@ func qcow2Pipelines(t *imageType, customizations *blueprint.Customizations, opti
 	pipelines = append(pipelines, *treePipeline)
 
 	diskfile := "disk.img"
-	kernelVer := kernelVerStr(packageSetSpecs[blueprintPkgsKey], customizations.GetKernel().Name, t.Arch().Name())
+	kernelVer := rpmmd.GetVerStrFromPackageSpecListPanic(packageSetSpecs[blueprintPkgsKey], customizations.GetKernel().Name)
 	imagePipeline := liveImagePipeline(treePipeline.Name, diskfile, &partitionTable, t.arch, kernelVer)
 	pipelines = append(pipelines, *imagePipeline)
 
@@ -47,7 +47,7 @@ func prependKernelCmdlineStage(pipeline *osbuild.Pipeline, t *imageType, pt *dis
 		if rootPartition == nil {
 			panic("s390x image must have a root partition, this is a programming error")
 		}
-		kernelStage := osbuild.NewKernelCmdlineStage(kernelCmdlineStageOptions(rootPartition.Filesystem.UUID, t.kernelOptions))
+		kernelStage := osbuild.NewKernelCmdlineStage(osbuild.NewKernelCmdlineStageOptions(rootPartition.Filesystem.UUID, t.kernelOptions))
 		pipeline.Stages = append([]*osbuild.Stage{kernelStage}, pipeline.Stages...)
 	}
 	return pipeline
@@ -69,7 +69,7 @@ func vhdPipelines(t *imageType, customizations *blueprint.Customizations, option
 	pipelines = append(pipelines, *treePipeline)
 
 	diskfile := "disk.img"
-	kernelVer := kernelVerStr(packageSetSpecs[blueprintPkgsKey], customizations.GetKernel().Name, t.Arch().Name())
+	kernelVer := rpmmd.GetVerStrFromPackageSpecListPanic(packageSetSpecs[blueprintPkgsKey], customizations.GetKernel().Name)
 	imagePipeline := liveImagePipeline(treePipeline.Name, diskfile, &partitionTable, t.arch, kernelVer)
 	pipelines = append(pipelines, *imagePipeline)
 	if err != nil {
@@ -97,7 +97,7 @@ func vmdkPipelines(t *imageType, customizations *blueprint.Customizations, optio
 	pipelines = append(pipelines, *treePipeline)
 
 	diskfile := "disk.img"
-	kernelVer := kernelVerStr(packageSetSpecs[blueprintPkgsKey], customizations.GetKernel().Name, t.Arch().Name())
+	kernelVer := rpmmd.GetVerStrFromPackageSpecListPanic(packageSetSpecs[blueprintPkgsKey], customizations.GetKernel().Name)
 	imagePipeline := liveImagePipeline(treePipeline.Name, diskfile, &partitionTable, t.arch, kernelVer)
 	pipelines = append(pipelines, *imagePipeline)
 	if err != nil {
@@ -125,7 +125,7 @@ func openstackPipelines(t *imageType, customizations *blueprint.Customizations,
 	pipelines = append(pipelines, *treePipeline)
 
 	diskfile := "disk.img"
-	kernelVer := kernelVerStr(packageSetSpecs[blueprintPkgsKey], customizations.GetKernel().Name, t.Arch().Name())
+	kernelVer := rpmmd.GetVerStrFromPackageSpecListPanic(packageSetSpecs[blueprintPkgsKey], customizations.GetKernel().Name)
 	imagePipeline := liveImagePipeline(treePipeline.Name, diskfile, &partitionTable, t.arch, kernelVer)
 	pipelines = append(pipelines, *imagePipeline)
 	if err != nil {
@@ -153,8 +153,7 @@ func ec2CommonPipelines(t *imageType, customizations *blueprint.Customizations,
 		return nil, err
 	}
 	pipelines = append(pipelines, *treePipeline)
-
-	kernelVer := kernelVerStr(packageSetSpecs[blueprintPkgsKey], customizations.GetKernel().Name, t.Arch().Name())
+	kernelVer := rpmmd.GetVerStrFromPackageSpecListPanic(packageSetSpecs[blueprintPkgsKey], customizations.GetKernel().Name)
 	imagePipeline := liveImagePipeline(treePipeline.Name, diskfile, &partitionTable, t.arch, kernelVer)
 	pipelines = append(pipelines, *imagePipeline)
 	return pipelines, nil
@@ -211,7 +210,7 @@ func edgeInstallerPipelines(t *imageType, customizations *blueprint.Customizatio
 	installerPackages := packageSetSpecs[installerPkgsKey]
 	d := t.arch.distro
 	archName := t.arch.name
-	kernelVer := kernelVerStr(installerPackages, "kernel", archName)
+	kernelVer := rpmmd.GetVerStrFromPackageSpecListPanic(installerPackages, "kernel")
 	ostreeRepoPath := "/ostree/repo"
 	payloadStages := ostreePayloadStages(options, ostreeRepoPath)
 	kickstartOptions := ostreeKickstartStageOptions(makeISORootPath(ostreeRepoPath), options.OSTree.Ref)
@@ -347,7 +346,7 @@ func buildPipeline(repos []rpmmd.RepoConfig, buildPackageSpecs []rpmmd.PackageSp
 	p := new(osbuild.Pipeline)
 	p.Name = "build"
 	p.Runner = runner
-	p.AddStage(osbuild.NewRPMStage(rpmStageOptions(repos), rpmStageInputs(buildPackageSpecs)))
+	p.AddStage(osbuild.NewRPMStage(rpmStageOptions(repos), osbuild.NewRpmStageSourceFilesInputs(buildPackageSpecs)))
 	p.AddStage(osbuild.NewSELinuxStage(selinuxStageOptions(true)))
 	return p
 }
@@ -373,7 +372,7 @@ func osPipeline(t *imageType,
 		p.AddStage(osbuild.NewOSTreePasswdStage("org.osbuild.source", options.OSTree.Parent))
 	}
 
-	p.AddStage(osbuild.NewRPMStage(rpmStageOptions(repos), rpmStageInputs(packages)))
+	p.AddStage(osbuild.NewRPMStage(rpmStageOptions(repos), osbuild.NewRpmStageSourceFilesInputs(packages)))
 
 	// If the /boot is on a separate partition, the prefix for the BLS stage must be ""
 	if pt == nil || pt.BootPartition() == nil {
@@ -522,7 +521,7 @@ func osPipeline(t *imageType,
 	if pt != nil {
 		p = prependKernelCmdlineStage(p, t, pt)
 		p.AddStage(osbuild.NewFSTabStage(pt.FSTabStageOptionsV2()))
-		kernelVer := kernelVerStr(bpPackages, c.GetKernel().Name, t.Arch().Name())
+		kernelVer := rpmmd.GetVerStrFromPackageSpecListPanic(bpPackages, c.GetKernel().Name)
 		p.AddStage(bootloaderConfigStage(t, *pt, c.GetKernel(), kernelVer, false, false))
 	}
 
@@ -574,7 +573,7 @@ func containerTreePipeline(repos []rpmmd.RepoConfig, packages []rpmmd.PackageSpe
 	p := new(osbuild.Pipeline)
 	p.Name = "container-tree"
 	p.Build = "name:build"
-	p.AddStage(osbuild.NewRPMStage(rpmStageOptions(repos), rpmStageInputs(packages)))
+	p.AddStage(osbuild.NewRPMStage(rpmStageOptions(repos), osbuild.NewRpmStageSourceFilesInputs(packages)))
 	language, _ := c.GetPrimaryLocale()
 	if language != nil {
 		p.AddStage(osbuild.NewLocaleStage(&osbuild.LocaleStageOptions{Language: *language}))
@@ -588,7 +587,7 @@ func containerTreePipeline(repos []rpmmd.RepoConfig, packages []rpmmd.PackageSpe
 
 	p.AddStage(osbuild.NewOSTreePullStage(
 		&osbuild.OSTreePullStageOptions{Repo: repoPath},
-		ostreePullStageInputs("org.osbuild.pipeline", "name:ostree-commit", options.OSTree.Ref),
+		osbuild.NewOstreePullStageInputs("org.osbuild.pipeline", "name:ostree-commit", options.OSTree.Ref),
 	))
 
 	// make nginx log directory world writeable, otherwise nginx can't start in
@@ -627,7 +626,7 @@ func ostreePayloadStages(options distro.ImageOptions, ostreeRepoPath string) []*
 	stages = append(stages, osbuild.NewOSTreeInitStage(&osbuild.OSTreeInitStageOptions{Path: ostreeRepoPath}))
 	stages = append(stages, osbuild.NewOSTreePullStage(
 		&osbuild.OSTreePullStageOptions{Repo: ostreeRepoPath},
-		ostreePullStageInputs("org.osbuild.source", options.OSTree.Parent, options.OSTree.Ref),
+		osbuild.NewOstreePullStageInputs("org.osbuild.source", options.OSTree.Parent, options.OSTree.Ref),
 	))
 
 	return stages
@@ -637,7 +636,7 @@ func edgeSimplifiedInstallerPipelines(t *imageType, customizations *blueprint.Cu
 	pipelines := make([]osbuild.Pipeline, 0)
 	pipelines = append(pipelines, *buildPipeline(repos, packageSetSpecs[buildPkgsKey], t.arch.distro.runner))
 	installerPackages := packageSetSpecs[installerPkgsKey]
-	kernelVer := kernelVerStr(installerPackages, "kernel", t.Arch().Name())
+	kernelVer := rpmmd.GetVerStrFromPackageSpecListPanic(installerPackages, "kernel")
 	imgName := "disk.img.xz"
 	installDevice := customizations.GetInstallationDevice()
 
@@ -717,12 +716,12 @@ func simplifiedInstallerBootISOTreePipeline(archivePipelineName, kver string) *o
 	}
 
 	inputName := "root-tree"
-	copyInputs := copyPipelineTreeInputs(inputName, "efiboot-tree")
+	copyInputs := osbuild.NewCopyStagePipelineTreeInputs(inputName, "efiboot-tree")
 	copyOptions, copyDevices, copyMounts := copyFSTreeOptions(inputName, "efiboot-tree", &pt, loopback)
 	p.AddStage(osbuild.NewCopyStage(copyOptions, copyInputs, copyDevices, copyMounts))
 
 	inputName = "coi"
-	copyInputs = copyPipelineTreeInputs(inputName, "coi-tree")
+	copyInputs = osbuild.NewCopyStagePipelineTreeInputs(inputName, "coi-tree")
 	p.AddStage(osbuild.NewCopyStageSimple(
 		&osbuild.CopyStageOptions{
 			Paths: []osbuild.CopyStagePath{
@@ -740,7 +739,7 @@ func simplifiedInstallerBootISOTreePipeline(archivePipelineName, kver string) *o
 	))
 
 	inputName = "efi-tree"
-	copyInputs = copyPipelineTreeInputs(inputName, "efiboot-tree")
+	copyInputs = osbuild.NewCopyStagePipelineTreeInputs(inputName, "efiboot-tree")
 	p.AddStage(osbuild.NewCopyStageSimple(
 		&osbuild.CopyStageOptions{
 			Paths: []osbuild.CopyStagePath{
@@ -768,7 +767,7 @@ func simplifiedInstallerTreePipeline(repos []rpmmd.RepoConfig, packages []rpmmd.
 	p := new(osbuild.Pipeline)
 	p.Name = "coi-tree"
 	p.Build = "name:build"
-	p.AddStage(osbuild.NewRPMStage(rpmStageOptions(repos), rpmStageInputs(packages)))
+	p.AddStage(osbuild.NewRPMStage(rpmStageOptions(repos), osbuild.NewRpmStageSourceFilesInputs(packages)))
 	p.AddStage(osbuild.NewBuildstampStage(buildStampStageOptions(arch, product, osVersion, variant)))
 	p.AddStage(osbuild.NewLocaleStage(&osbuild.LocaleStageOptions{Language: "en_US.UTF-8"}))
 	p.AddStage(osbuild.NewDracutStage(dracutStageOptions(kernelVer, arch, []string{"coreos-installer"})))
@@ -795,7 +794,7 @@ func ostreeDeployPipeline(
 	p.AddStage(osbuild.OSTreeInitFsStage())
 	p.AddStage(osbuild.NewOSTreePullStage(
 		&osbuild.OSTreePullStageOptions{Repo: repoPath, Remote: remote},
-		ostreePullStageInputs("org.osbuild.source", options.OSTree.Parent, options.OSTree.Ref),
+		osbuild.NewOstreePullStageInputs("org.osbuild.source", options.OSTree.Parent, options.OSTree.Ref),
 	))
 	p.AddStage(osbuild.NewOSTreeOsInitStage(
 		&osbuild.OSTreeOsInitStageOptions{
@@ -871,7 +870,7 @@ func anacondaTreePipeline(repos []rpmmd.RepoConfig, packages []rpmmd.PackageSpec
 	p := new(osbuild.Pipeline)
 	p.Name = "anaconda-tree"
 	p.Build = "name:build"
-	p.AddStage(osbuild.NewRPMStage(rpmStageOptions(repos), rpmStageInputs(packages)))
+	p.AddStage(osbuild.NewRPMStage(rpmStageOptions(repos), osbuild.NewRpmStageSourceFilesInputs(packages)))
 	p.AddStage(osbuild.NewBuildstampStage(buildStampStageOptions(arch, product, osVersion, variant)))
 	p.AddStage(osbuild.NewLocaleStage(&osbuild.LocaleStageOptions{Language: "en_US.UTF-8"}))
 
@@ -922,7 +921,7 @@ func bootISOTreePipeline(kernelVer, arch, vendor, product, osVersion, isolabel s
 	p.Name = "bootiso-tree"
 	p.Build = "name:build"
 
-	p.AddStage(osbuild.NewBootISOMonoStage(bootISOMonoStageOptions(kernelVer, arch, vendor, product, osVersion, isolabel), bootISOMonoStageInputs()))
+	p.AddStage(osbuild.NewBootISOMonoStage(bootISOMonoStageOptions(kernelVer, arch, vendor, product, osVersion, isolabel), osbuild.NewBootISOMonoStagePipelineTreeInputs("anaconda-tree")))
 	p.AddStage(osbuild.NewKickstartStage(ksOptions))
 	p.AddStage(osbuild.NewDiscinfoStage(discinfoStageOptions(arch)))
 
@@ -937,7 +936,7 @@ func bootISOPipeline(filename, isolabel, arch string, isolinux bool) *osbuild.Pi
 	p.Name = "bootiso"
 	p.Build = "name:build"
 
-	p.AddStage(osbuild.NewXorrisofsStage(xorrisofsStageOptions(filename, isolabel, arch, isolinux), xorrisofsStageInputs("bootiso-tree")))
+	p.AddStage(osbuild.NewXorrisofsStage(xorrisofsStageOptions(filename, isolabel, arch, isolinux), osbuild.NewXorrisofsStagePipelineTreeInputs("bootiso-tree")))
 	p.AddStage(osbuild.NewImplantisomd5Stage(&osbuild.Implantisomd5StageOptions{Filename: filename}))
 
 	return p
@@ -959,7 +958,7 @@ func liveImagePipeline(inputPipelineName string, outputFilename string, pt *disk
 
 	inputName := "root-tree"
 	copyOptions, copyDevices, copyMounts := copyFSTreeOptions(inputName, inputPipelineName, pt, loopback)
-	copyInputs := copyPipelineTreeInputs(inputName, inputPipelineName)
+	copyInputs := osbuild.NewCopyStagePipelineTreeInputs(inputName, inputPipelineName)
 	p.AddStage(osbuild.NewCopyStage(copyOptions, copyInputs, copyDevices, copyMounts))
 	p.AddStage(bootloaderInstStage(outputFilename, pt, arch, kernelVer, copyDevices, copyMounts, loopback))
 	return p
@@ -1040,7 +1039,7 @@ func qemuPipeline(inputPipelineName, inputFilename, outputFilename, format, qcow
 	p.Name = format
 	p.Build = "name:build"
 
-	qemuStage := osbuild.NewQEMUStage(qemuStageOptions(outputFilename, format, qcow2Compat), qemuStageInputs(inputPipelineName, inputFilename))
+	qemuStage := osbuild.NewQEMUStage(qemuStageOptions(outputFilename, format, qcow2Compat), osbuild.NewQemuStagePipelineFilesInputs(inputPipelineName, inputFilename))
 	p.AddStage(qemuStage)
 	return p
 }
@@ -1072,19 +1071,3 @@ func bootloaderInstStage(filename string, pt *disk.PartitionTable, arch *archite
 
 	return nil
 }
-
-func kernelVerStr(pkgs []rpmmd.PackageSpec, kernelName, arch string) string {
-	kernelPkg := new(rpmmd.PackageSpec)
-	for _, pkg := range pkgs {
-		if pkg.Name == kernelName {
-			// Implicit memory alasing doesn't couse any bug in this case
-			/* #nosec G601 */
-			kernelPkg = &pkg
-			break
-		}
-	}
-	if kernelPkg == nil {
-		panic(fmt.Sprintf("kernel package %q not found", kernelName))
-	}
-	return fmt.Sprintf("%s-%s.%s", kernelPkg.Version, kernelPkg.Release, kernelPkg.Arch)
-}
diff --git a/internal/distro/rhel86/stage_inputs.go b/internal/distro/rhel86/stage_inputs.go
deleted file mode 100644
index 206c6bc7e..000000000
--- a/internal/distro/rhel86/stage_inputs.go
+++ /dev/null
@@ -1,74 +0,0 @@
-package rhel86
-
-import (
-	osbuild "github.com/osbuild/osbuild-composer/internal/osbuild2"
-	"github.com/osbuild/osbuild-composer/internal/rpmmd"
-)
-
-func bootISOMonoStageInputs() *osbuild.BootISOMonoStageInputs {
-	rootfsInput := new(osbuild.BootISOMonoStageInput)
-	rootfsInput.Type = "org.osbuild.tree"
-	rootfsInput.Origin = "org.osbuild.pipeline"
-	rootfsInput.References = osbuild.BootISOMonoStageReferences{"name:anaconda-tree"}
-	return &osbuild.BootISOMonoStageInputs{
-		RootFS: rootfsInput,
-	}
-}
-
-func rpmStageInputs(specs []rpmmd.PackageSpec) *osbuild.RPMStageInputs {
-	stageInput := new(osbuild.RPMStageInput)
-	stageInput.Type = "org.osbuild.files"
-	stageInput.Origin = "org.osbuild.source"
-	stageInput.References = pkgRefs(specs)
-	return &osbuild.RPMStageInputs{Packages: stageInput}
-}
-
-func pkgRefs(specs []rpmmd.PackageSpec) osbuild.RPMStageReferences {
-	refs := make([]string, len(specs))
-	for idx, pkg := range specs {
-		refs[idx] = pkg.Checksum
-	}
-	return refs
-}
-
-func ostreePullStageInputs(origin, source, commitRef string) *osbuild.OSTreePullStageInputs {
-	pullStageInput := new(osbuild.OSTreePullStageInput)
-	pullStageInput.Type = "org.osbuild.ostree"
-	pullStageInput.Origin = origin
-
-	inputRefs := make(map[string]osbuild.OSTreePullStageReference)
-	inputRefs[source] = osbuild.OSTreePullStageReference{Ref: commitRef}
-	pullStageInput.References = inputRefs
-	return &osbuild.OSTreePullStageInputs{Commits: pullStageInput}
-}
-
-func xorrisofsStageInputs(pipeline string) *osbuild.XorrisofsStageInputs {
-	input := new(osbuild.XorrisofsStageInput)
-	input.Type = "org.osbuild.tree"
-	input.Origin = "org.osbuild.pipeline"
-	input.References = osbuild.XorrisofsStageReferences{"name:" + pipeline}
-	return &osbuild.XorrisofsStageInputs{Tree: input}
-}
-
-func copyPipelineTreeInputs(name, inputPipeline string) *osbuild.CopyStageInputs {
-	inputName := name
-	treeInput := osbuild.CopyStageInput{}
-	treeInput.Type = "org.osbuild.tree"
-	treeInput.Origin = "org.osbuild.pipeline"
-	treeInput.References = []string{"name:" + inputPipeline}
-	return &osbuild.CopyStageInputs{inputName: treeInput}
-}
-
-func qemuStageInputs(stage, file string) *osbuild.QEMUStageInputs {
-	stageKey := "name:" + stage
-	ref := map[string]osbuild.QEMUFile{
-		stageKey: {
-			File: file,
-		},
-	}
-	input := new(osbuild.QEMUStageInput)
-	input.Type = "org.osbuild.files"
-	input.Origin = "org.osbuild.pipeline"
-	input.References = ref
-	return &osbuild.QEMUStageInputs{Image: input}
-}
diff --git a/internal/distro/rhel86/stage_options.go b/internal/distro/rhel86/stage_options.go
index 7d242f9f4..aa2d8c9c0 100644
--- a/internal/distro/rhel86/stage_options.go
+++ b/internal/distro/rhel86/stage_options.go
@@ -573,13 +573,6 @@ func qemuStageOptions(filename, format, compat string) *osbuild.QEMUStageOptions
 	}
 }
 
-func kernelCmdlineStageOptions(rootUUID string, kernelOptions string) *osbuild.KernelCmdlineStageOptions {
-	return &osbuild.KernelCmdlineStageOptions{
-		RootFsUUID: rootUUID,
-		KernelOpts: kernelOptions,
-	}
-}
-
 func nginxConfigStageOptions(path, htmlRoot, listen string) *osbuild.NginxConfigStageOptions {
 	// configure nginx to work in an unprivileged container
 	cfg := &osbuild.NginxConfig{
diff --git a/internal/distro/rhel90/pipelines.go b/internal/distro/rhel90/pipelines.go
index e9dddb138..bc46e9a56 100644
--- a/internal/distro/rhel90/pipelines.go
+++ b/internal/distro/rhel90/pipelines.go
@@ -31,7 +31,7 @@ func qcow2Pipelines(t *imageType, customizations *blueprint.Customizations, opti
 	pipelines = append(pipelines, *treePipeline)
 
 	diskfile := "disk.img"
-	kernelVer := kernelVerStr(packageSetSpecs[blueprintPkgsKey], customizations.GetKernel().Name, t.Arch().Name())
+	kernelVer := rpmmd.GetVerStrFromPackageSpecListPanic(packageSetSpecs[blueprintPkgsKey], customizations.GetKernel().Name)
 	imagePipeline := liveImagePipeline(treePipeline.Name, diskfile, &partitionTable, t.arch, kernelVer)
 	pipelines = append(pipelines, *imagePipeline)
 
@@ -43,7 +43,7 @@ func qcow2Pipelines(t *imageType, customizations *blueprint.Customizations, opti
 
 func prependKernelCmdlineStage(pipeline *osbuild.Pipeline, t *imageType, pt *disk.PartitionTable) *osbuild.Pipeline {
 	rootFsUUID := pt.RootPartition().Filesystem.UUID
-	kernelStage := osbuild.NewKernelCmdlineStage(kernelCmdlineStageOptions(rootFsUUID, t.kernelOptions))
+	kernelStage := osbuild.NewKernelCmdlineStage(osbuild.NewKernelCmdlineStageOptions(rootFsUUID, t.kernelOptions))
 	pipeline.Stages = append([]*osbuild.Stage{kernelStage}, pipeline.Stages...)
 	return pipeline
 }
@@ -64,7 +64,7 @@ func vhdPipelines(t *imageType, customizations *blueprint.Customizations, option
 	pipelines = append(pipelines, *treePipeline)
 
 	diskfile := "disk.img"
-	kernelVer := kernelVerStr(packageSetSpecs[blueprintPkgsKey], customizations.GetKernel().Name, t.Arch().Name())
+	kernelVer := rpmmd.GetVerStrFromPackageSpecListPanic(packageSetSpecs[blueprintPkgsKey], customizations.GetKernel().Name)
 	imagePipeline := liveImagePipeline(treePipeline.Name, diskfile, &partitionTable, t.arch, kernelVer)
 	pipelines = append(pipelines, *imagePipeline)
 
@@ -89,7 +89,7 @@ func vmdkPipelines(t *imageType, customizations *blueprint.Customizations, optio
 	pipelines = append(pipelines, *treePipeline)
 
 	diskfile := "disk.img"
-	kernelVer := kernelVerStr(packageSetSpecs[blueprintPkgsKey], customizations.GetKernel().Name, t.Arch().Name())
+	kernelVer := rpmmd.GetVerStrFromPackageSpecListPanic(packageSetSpecs[blueprintPkgsKey], customizations.GetKernel().Name)
 	imagePipeline := liveImagePipeline(treePipeline.Name, diskfile, &partitionTable, t.arch, kernelVer)
 	pipelines = append(pipelines, *imagePipeline)
 
@@ -114,7 +114,7 @@ func openstackPipelines(t *imageType, customizations *blueprint.Customizations,
 	pipelines = append(pipelines, *treePipeline)
 
 	diskfile := "disk.img"
-	kernelVer := kernelVerStr(packageSetSpecs[blueprintPkgsKey], customizations.GetKernel().Name, t.Arch().Name())
+	kernelVer := rpmmd.GetVerStrFromPackageSpecListPanic(packageSetSpecs[blueprintPkgsKey], customizations.GetKernel().Name)
 	imagePipeline := liveImagePipeline(treePipeline.Name, diskfile, &partitionTable, t.arch, kernelVer)
 	pipelines = append(pipelines, *imagePipeline)
 
@@ -140,7 +140,7 @@ func ec2CommonPipelines(t *imageType, customizations *blueprint.Customizations,
 	}
 	pipelines = append(pipelines, *treePipeline)
 
-	kernelVer := kernelVerStr(packageSetSpecs[blueprintPkgsKey], customizations.GetKernel().Name, t.Arch().Name())
+	kernelVer := rpmmd.GetVerStrFromPackageSpecListPanic(packageSetSpecs[blueprintPkgsKey], customizations.GetKernel().Name)
 	imagePipeline := liveImagePipeline(treePipeline.Name, diskfile, &partitionTable, t.arch, kernelVer)
 	pipelines = append(pipelines, *imagePipeline)
 	return pipelines, nil
@@ -197,7 +197,7 @@ func edgeInstallerPipelines(t *imageType, customizations *blueprint.Customizatio
 	installerPackages := packageSetSpecs[installerPkgsKey]
 	d := t.arch.distro
 	archName := t.arch.name
-	kernelVer := kernelVerStr(installerPackages, "kernel", archName)
+	kernelVer := rpmmd.GetVerStrFromPackageSpecListPanic(installerPackages, "kernel")
 	ostreeRepoPath := "/ostree/repo"
 	payloadStages := ostreePayloadStages(options, ostreeRepoPath)
 	kickstartOptions := ostreeKickstartStageOptions(makeISORootPath(ostreeRepoPath), options.OSTree.Ref)
@@ -333,7 +333,7 @@ func buildPipeline(repos []rpmmd.RepoConfig, buildPackageSpecs []rpmmd.PackageSp
 	p := new(osbuild.Pipeline)
 	p.Name = "build"
 	p.Runner = runner
-	p.AddStage(osbuild.NewRPMStage(rpmStageOptions(repos), rpmStageInputs(buildPackageSpecs)))
+	p.AddStage(osbuild.NewRPMStage(rpmStageOptions(repos), osbuild.NewRpmStageSourceFilesInputs(buildPackageSpecs)))
 	p.AddStage(osbuild.NewSELinuxStage(selinuxStageOptions(true)))
 	return p
 }
@@ -359,7 +359,7 @@ func osPipeline(t *imageType,
 		p.AddStage(osbuild.NewOSTreePasswdStage("org.osbuild.source", options.OSTree.Parent))
 	}
 
-	p.AddStage(osbuild.NewRPMStage(rpmStageOptions(repos), rpmStageInputs(packages)))
+	p.AddStage(osbuild.NewRPMStage(rpmStageOptions(repos), osbuild.NewRpmStageSourceFilesInputs(packages)))
 
 	// If the /boot is on a separate partition, the prefix for the BLS stage must be ""
 	if pt == nil || pt.BootPartition() == nil {
@@ -508,7 +508,7 @@ func osPipeline(t *imageType,
 	if pt != nil {
 		p = prependKernelCmdlineStage(p, t, pt)
 		p.AddStage(osbuild.NewFSTabStage(pt.FSTabStageOptionsV2()))
-		kernelVer := kernelVerStr(bpPackages, c.GetKernel().Name, t.Arch().Name())
+		kernelVer := rpmmd.GetVerStrFromPackageSpecListPanic(bpPackages, c.GetKernel().Name)
 		p.AddStage(bootloaderConfigStage(t, *pt, c.GetKernel(), kernelVer, false, false))
 	}
 
@@ -560,7 +560,7 @@ func containerTreePipeline(repos []rpmmd.RepoConfig, packages []rpmmd.PackageSpe
 	p := new(osbuild.Pipeline)
 	p.Name = "container-tree"
 	p.Build = "name:build"
-	p.AddStage(osbuild.NewRPMStage(rpmStageOptions(repos), rpmStageInputs(packages)))
+	p.AddStage(osbuild.NewRPMStage(rpmStageOptions(repos), osbuild.NewRpmStageSourceFilesInputs(packages)))
 	language, _ := c.GetPrimaryLocale()
 	if language != nil {
 		p.AddStage(osbuild.NewLocaleStage(&osbuild.LocaleStageOptions{Language: *language}))
@@ -574,7 +574,7 @@ func containerTreePipeline(repos []rpmmd.RepoConfig, packages []rpmmd.PackageSpe
 
 	p.AddStage(osbuild.NewOSTreePullStage(
 		&osbuild.OSTreePullStageOptions{Repo: repoPath},
-		ostreePullStageInputs("org.osbuild.pipeline", "name:ostree-commit", options.OSTree.Ref),
+		osbuild.NewOstreePullStageInputs("org.osbuild.pipeline", "name:ostree-commit", options.OSTree.Ref),
 	))
 
 	// make nginx log and lib directories world writeable, otherwise nginx can't start in
@@ -614,7 +614,7 @@ func ostreePayloadStages(options distro.ImageOptions, ostreeRepoPath string) []*
 	stages = append(stages, osbuild.NewOSTreeInitStage(&osbuild.OSTreeInitStageOptions{Path: ostreeRepoPath}))
 	stages = append(stages, osbuild.NewOSTreePullStage(
 		&osbuild.OSTreePullStageOptions{Repo: ostreeRepoPath},
-		ostreePullStageInputs("org.osbuild.source", options.OSTree.Parent, options.OSTree.Ref),
+		osbuild.NewOstreePullStageInputs("org.osbuild.source", options.OSTree.Parent, options.OSTree.Ref),
 	))
 
 	return stages
@@ -624,7 +624,7 @@ func edgeSimplifiedInstallerPipelines(t *imageType, customizations *blueprint.Cu
 	pipelines := make([]osbuild.Pipeline, 0)
 	pipelines = append(pipelines, *buildPipeline(repos, packageSetSpecs[buildPkgsKey], t.arch.distro.runner))
 	installerPackages := packageSetSpecs[installerPkgsKey]
-	kernelVer := kernelVerStr(installerPackages, "kernel", t.Arch().Name())
+	kernelVer := rpmmd.GetVerStrFromPackageSpecListPanic(installerPackages, "kernel")
 	imgName := "disk.img.xz"
 	installDevice := customizations.GetInstallationDevice()
 
@@ -713,12 +713,12 @@ func simplifiedInstallerBootISOTreePipeline(archivePipelineName, kver string, rn
 	}
 
 	inputName := "root-tree"
-	copyInputs := copyPipelineTreeInputs(inputName, "efiboot-tree")
+	copyInputs := osbuild.NewCopyStagePipelineTreeInputs(inputName, "efiboot-tree")
 	copyOptions, copyDevices, copyMounts := copyFSTreeOptions(inputName, "efiboot-tree", &pt, loopback)
 	p.AddStage(osbuild.NewCopyStage(copyOptions, copyInputs, copyDevices, copyMounts))
 
 	inputName = "coi"
-	copyInputs = copyPipelineTreeInputs(inputName, "coi-tree")
+	copyInputs = osbuild.NewCopyStagePipelineTreeInputs(inputName, "coi-tree")
 	p.AddStage(osbuild.NewCopyStageSimple(
 		&osbuild.CopyStageOptions{
 			Paths: []osbuild.CopyStagePath{
@@ -736,7 +736,7 @@ func simplifiedInstallerBootISOTreePipeline(archivePipelineName, kver string, rn
 	))
 
 	inputName = "efi-tree"
-	copyInputs = copyPipelineTreeInputs(inputName, "efiboot-tree")
+	copyInputs = osbuild.NewCopyStagePipelineTreeInputs(inputName, "efiboot-tree")
 	p.AddStage(osbuild.NewCopyStageSimple(
 		&osbuild.CopyStageOptions{
 			Paths: []osbuild.CopyStagePath{
@@ -764,7 +764,7 @@ func simplifiedInstallerTreePipeline(repos []rpmmd.RepoConfig, packages []rpmmd.
 	p := new(osbuild.Pipeline)
 	p.Name = "coi-tree"
 	p.Build = "name:build"
-	p.AddStage(osbuild.NewRPMStage(rpmStageOptions(repos), rpmStageInputs(packages)))
+	p.AddStage(osbuild.NewRPMStage(rpmStageOptions(repos), osbuild.NewRpmStageSourceFilesInputs(packages)))
 	p.AddStage(osbuild.NewBuildstampStage(buildStampStageOptions(arch, product, osVersion, variant)))
 	p.AddStage(osbuild.NewLocaleStage(&osbuild.LocaleStageOptions{Language: "en_US.UTF-8"}))
 	p.AddStage(osbuild.NewDracutStage(dracutStageOptions(kernelVer, arch, []string{"coreos-installer"})))
@@ -791,7 +791,7 @@ func ostreeDeployPipeline(
 	p.AddStage(osbuild.OSTreeInitFsStage())
 	p.AddStage(osbuild.NewOSTreePullStage(
 		&osbuild.OSTreePullStageOptions{Repo: repoPath, Remote: remote},
-		ostreePullStageInputs("org.osbuild.source", options.OSTree.Parent, options.OSTree.Ref),
+		osbuild.NewOstreePullStageInputs("org.osbuild.source", options.OSTree.Parent, options.OSTree.Ref),
 	))
 	p.AddStage(osbuild.NewOSTreeOsInitStage(
 		&osbuild.OSTreeOsInitStageOptions{
@@ -867,7 +867,7 @@ func anacondaTreePipeline(repos []rpmmd.RepoConfig, packages []rpmmd.PackageSpec
 	p := new(osbuild.Pipeline)
 	p.Name = "anaconda-tree"
 	p.Build = "name:build"
-	p.AddStage(osbuild.NewRPMStage(rpmStageOptions(repos), rpmStageInputs(packages)))
+	p.AddStage(osbuild.NewRPMStage(rpmStageOptions(repos), osbuild.NewRpmStageSourceFilesInputs(packages)))
 	p.AddStage(osbuild.NewBuildstampStage(buildStampStageOptions(arch, product, osVersion, variant)))
 	p.AddStage(osbuild.NewLocaleStage(&osbuild.LocaleStageOptions{Language: "en_US.UTF-8"}))
 
@@ -918,7 +918,7 @@ func bootISOTreePipeline(kernelVer, arch, vendor, product, osVersion, isolabel s
 	p.Name = "bootiso-tree"
 	p.Build = "name:build"
 
-	p.AddStage(osbuild.NewBootISOMonoStage(bootISOMonoStageOptions(kernelVer, arch, vendor, product, osVersion, isolabel), bootISOMonoStageInputs()))
+	p.AddStage(osbuild.NewBootISOMonoStage(bootISOMonoStageOptions(kernelVer, arch, vendor, product, osVersion, isolabel), osbuild.NewBootISOMonoStagePipelineTreeInputs("anaconda-tree")))
 	p.AddStage(osbuild.NewKickstartStage(ksOptions))
 	p.AddStage(osbuild.NewDiscinfoStage(discinfoStageOptions(arch)))
 
@@ -933,7 +933,7 @@ func bootISOPipeline(filename, isolabel, arch string, isolinux bool) *osbuild.Pi
 	p.Name = "bootiso"
 	p.Build = "name:build"
 
-	p.AddStage(osbuild.NewXorrisofsStage(xorrisofsStageOptions(filename, isolabel, arch, isolinux), xorrisofsStageInputs("bootiso-tree")))
+	p.AddStage(osbuild.NewXorrisofsStage(xorrisofsStageOptions(filename, isolabel, arch, isolinux), osbuild.NewXorrisofsStagePipelineTreeInputs("bootiso-tree")))
 	p.AddStage(osbuild.NewImplantisomd5Stage(&osbuild.Implantisomd5StageOptions{Filename: filename}))
 
 	return p
@@ -955,7 +955,7 @@ func liveImagePipeline(inputPipelineName string, outputFilename string, pt *disk
 
 	inputName := "root-tree"
 	copyOptions, copyDevices, copyMounts := copyFSTreeOptions(inputName, inputPipelineName, pt, loopback)
-	copyInputs := copyPipelineTreeInputs(inputName, inputPipelineName)
+	copyInputs := osbuild.NewCopyStagePipelineTreeInputs(inputName, inputPipelineName)
 	p.AddStage(osbuild.NewCopyStage(copyOptions, copyInputs, copyDevices, copyMounts))
 	p.AddStage(bootloaderInstStage(outputFilename, pt, arch, kernelVer, copyDevices, copyMounts, loopback))
 	return p
@@ -1036,7 +1036,7 @@ func qemuPipeline(inputPipelineName, inputFilename, outputFilename, format, qcow
 	p.Name = format
 	p.Build = "name:build"
 
-	qemuStage := osbuild.NewQEMUStage(qemuStageOptions(outputFilename, format, qcow2Compat), qemuStageInputs(inputPipelineName, inputFilename))
+	qemuStage := osbuild.NewQEMUStage(qemuStageOptions(outputFilename, format, qcow2Compat), osbuild.NewQemuStagePipelineFilesInputs(inputPipelineName, inputFilename))
 	p.AddStage(qemuStage)
 	return p
 }
@@ -1068,19 +1068,3 @@ func bootloaderInstStage(filename string, pt *disk.PartitionTable, arch *archite
 
 	return nil
 }
-
-func kernelVerStr(pkgs []rpmmd.PackageSpec, kernelName, arch string) string {
-	kernelPkg := new(rpmmd.PackageSpec)
-	for _, pkg := range pkgs {
-		if pkg.Name == kernelName {
-			// Implicit memory alasing doesn't couse any bug in this case
-			/* #nosec G601 */
-			kernelPkg = &pkg
-			break
-		}
-	}
-	if kernelPkg == nil {
-		panic(fmt.Sprintf("kernel package %q not found", kernelName))
-	}
-	return fmt.Sprintf("%s-%s.%s", kernelPkg.Version, kernelPkg.Release, kernelPkg.Arch)
-}
diff --git a/internal/distro/rhel90/stage_inputs.go b/internal/distro/rhel90/stage_inputs.go
deleted file mode 100644
index c1b27e311..000000000
--- a/internal/distro/rhel90/stage_inputs.go
+++ /dev/null
@@ -1,74 +0,0 @@
-package rhel90
-
-import (
-	osbuild "github.com/osbuild/osbuild-composer/internal/osbuild2"
-	"github.com/osbuild/osbuild-composer/internal/rpmmd"
-)
-
-func bootISOMonoStageInputs() *osbuild.BootISOMonoStageInputs {
-	rootfsInput := new(osbuild.BootISOMonoStageInput)
-	rootfsInput.Type = "org.osbuild.tree"
-	rootfsInput.Origin = "org.osbuild.pipeline"
-	rootfsInput.References = osbuild.BootISOMonoStageReferences{"name:anaconda-tree"}
-	return &osbuild.BootISOMonoStageInputs{
-		RootFS: rootfsInput,
-	}
-}
-
-func rpmStageInputs(specs []rpmmd.PackageSpec) *osbuild.RPMStageInputs {
-	stageInput := new(osbuild.RPMStageInput)
-	stageInput.Type = "org.osbuild.files"
-	stageInput.Origin = "org.osbuild.source"
-	stageInput.References = pkgRefs(specs)
-	return &osbuild.RPMStageInputs{Packages: stageInput}
-}
-
-func pkgRefs(specs []rpmmd.PackageSpec) osbuild.RPMStageReferences {
-	refs := make([]string, len(specs))
-	for idx, pkg := range specs {
-		refs[idx] = pkg.Checksum
-	}
-	return refs
-}
-
-func ostreePullStageInputs(origin, source, commitRef string) *osbuild.OSTreePullStageInputs {
-	pullStageInput := new(osbuild.OSTreePullStageInput)
-	pullStageInput.Type = "org.osbuild.ostree"
-	pullStageInput.Origin = origin
-
-	inputRefs := make(map[string]osbuild.OSTreePullStageReference)
-	inputRefs[source] = osbuild.OSTreePullStageReference{Ref: commitRef}
-	pullStageInput.References = inputRefs
-	return &osbuild.OSTreePullStageInputs{Commits: pullStageInput}
-}
-
-func xorrisofsStageInputs(pipeline string) *osbuild.XorrisofsStageInputs {
-	input := new(osbuild.XorrisofsStageInput)
-	input.Type = "org.osbuild.tree"
-	input.Origin = "org.osbuild.pipeline"
-	input.References = osbuild.XorrisofsStageReferences{"name:" + pipeline}
-	return &osbuild.XorrisofsStageInputs{Tree: input}
-}
-
-func copyPipelineTreeInputs(name, inputPipeline string) *osbuild.CopyStageInputs {
-	inputName := name
-	treeInput := osbuild.CopyStageInput{}
-	treeInput.Type = "org.osbuild.tree"
-	treeInput.Origin = "org.osbuild.pipeline"
-	treeInput.References = []string{"name:" + inputPipeline}
-	return &osbuild.CopyStageInputs{inputName: treeInput}
-}
-
-func qemuStageInputs(stage, file string) *osbuild.QEMUStageInputs {
-	stageKey := "name:" + stage
-	ref := map[string]osbuild.QEMUFile{
-		stageKey: {
-			File: file,
-		},
-	}
-	input := new(osbuild.QEMUStageInput)
-	input.Type = "org.osbuild.files"
-	input.Origin = "org.osbuild.pipeline"
-	input.References = ref
-	return &osbuild.QEMUStageInputs{Image: input}
-}
diff --git a/internal/distro/rhel90/stage_options.go b/internal/distro/rhel90/stage_options.go
index 3774b15d4..9095b4bec 100644
--- a/internal/distro/rhel90/stage_options.go
+++ b/internal/distro/rhel90/stage_options.go
@@ -574,13 +574,6 @@ func qemuStageOptions(filename, format, compat string) *osbuild.QEMUStageOptions
 	}
 }
 
-func kernelCmdlineStageOptions(rootUUID string, kernelOptions string) *osbuild.KernelCmdlineStageOptions {
-	return &osbuild.KernelCmdlineStageOptions{
-		RootFsUUID: rootUUID,
-		KernelOpts: kernelOptions,
-	}
-}
-
 func nginxConfigStageOptions(path, htmlRoot, listen string) *osbuild.NginxConfigStageOptions {
 	// configure nginx to work in an unprivileged container
 	cfg := &osbuild.NginxConfig{
diff --git a/internal/distro/rhel90beta/pipelines.go b/internal/distro/rhel90beta/pipelines.go
index 17b5a6d88..f9d95338f 100644
--- a/internal/distro/rhel90beta/pipelines.go
+++ b/internal/distro/rhel90beta/pipelines.go
@@ -9,7 +9,6 @@ import (
 	"github.com/osbuild/osbuild-composer/internal/common"
 	"github.com/osbuild/osbuild-composer/internal/disk"
 	"github.com/osbuild/osbuild-composer/internal/distro"
-	"github.com/osbuild/osbuild-composer/internal/osbuild2"
 	osbuild "github.com/osbuild/osbuild-composer/internal/osbuild2"
 	"github.com/osbuild/osbuild-composer/internal/rpmmd"
 )
@@ -44,7 +43,7 @@ func qcow2Pipelines(t *imageType, customizations *blueprint.Customizations, opti
 		}))
 	}
 	treePipeline.AddStage(osbuild.NewFSTabStage(partitionTable.FSTabStageOptionsV2()))
-	kernelVer := kernelVerStr(packageSetSpecs[blueprintPkgsKey], customizations.GetKernel().Name, t.Arch().Name())
+	kernelVer := rpmmd.GetVerStrFromPackageSpecListPanic(packageSetSpecs[blueprintPkgsKey], customizations.GetKernel().Name)
 	treePipeline.AddStage(bootloaderConfigStage(t, partitionTable, customizations.GetKernel(), kernelVer))
 	treePipeline.AddStage(osbuild.NewSELinuxStage(selinuxStageOptions(false)))
 	pipelines = append(pipelines, *treePipeline)
@@ -61,7 +60,7 @@ func qcow2Pipelines(t *imageType, customizations *blueprint.Customizations, opti
 
 func prependKernelCmdlineStage(pipeline *osbuild.Pipeline, t *imageType, pt *disk.PartitionTable) *osbuild.Pipeline {
 	rootFsUUID := pt.RootPartition().Filesystem.UUID
-	kernelStage := osbuild.NewKernelCmdlineStage(kernelCmdlineStageOptions(rootFsUUID, t.kernelOptions))
+	kernelStage := osbuild.NewKernelCmdlineStage(osbuild.NewKernelCmdlineStageOptions(rootFsUUID, t.kernelOptions))
 	pipeline.Stages = append([]*osbuild.Stage{kernelStage}, pipeline.Stages...)
 	return pipeline
 }
@@ -81,7 +80,7 @@ func vhdPipelines(t *imageType, customizations *blueprint.Customizations, option
 
 	treePipeline = prependKernelCmdlineStage(treePipeline, t, &partitionTable)
 	treePipeline.AddStage(osbuild.NewFSTabStage(partitionTable.FSTabStageOptionsV2()))
-	kernelVer := kernelVerStr(packageSetSpecs[blueprintPkgsKey], customizations.GetKernel().Name, t.Arch().Name())
+	kernelVer := rpmmd.GetVerStrFromPackageSpecListPanic(packageSetSpecs[blueprintPkgsKey], customizations.GetKernel().Name)
 	treePipeline.AddStage(bootloaderConfigStage(t, partitionTable, customizations.GetKernel(), kernelVer))
 	treePipeline.AddStage(osbuild.NewSELinuxStage(selinuxStageOptions(false)))
 	pipelines = append(pipelines, *treePipeline)
@@ -110,7 +109,7 @@ func vmdkPipelines(t *imageType, customizations *blueprint.Customizations, optio
 
 	treePipeline = prependKernelCmdlineStage(treePipeline, t, &partitionTable)
 	treePipeline.AddStage(osbuild.NewFSTabStage(partitionTable.FSTabStageOptionsV2()))
-	kernelVer := kernelVerStr(packageSetSpecs[blueprintPkgsKey], customizations.GetKernel().Name, t.Arch().Name())
+	kernelVer := rpmmd.GetVerStrFromPackageSpecListPanic(packageSetSpecs[blueprintPkgsKey], customizations.GetKernel().Name)
 	treePipeline.AddStage(bootloaderConfigStage(t, partitionTable, customizations.GetKernel(), kernelVer))
 	treePipeline.AddStage(osbuild.NewSELinuxStage(selinuxStageOptions(false)))
 	pipelines = append(pipelines, *treePipeline)
@@ -138,7 +137,7 @@ func openstackPipelines(t *imageType, customizations *blueprint.Customizations,
 	}
 	treePipeline = prependKernelCmdlineStage(treePipeline, t, &partitionTable)
 	treePipeline.AddStage(osbuild.NewFSTabStage(partitionTable.FSTabStageOptionsV2()))
-	kernelVer := kernelVerStr(packageSetSpecs[blueprintPkgsKey], customizations.GetKernel().Name, t.Arch().Name())
+	kernelVer := rpmmd.GetVerStrFromPackageSpecListPanic(packageSetSpecs[blueprintPkgsKey], customizations.GetKernel().Name)
 	treePipeline.AddStage(bootloaderConfigStage(t, partitionTable, customizations.GetKernel(), kernelVer))
 	treePipeline.AddStage(osbuild.NewSELinuxStage(selinuxStageOptions(false)))
 	pipelines = append(pipelines, *treePipeline)
@@ -169,7 +168,7 @@ func ec2BaseTreePipeline(repos []rpmmd.RepoConfig, packages []rpmmd.PackageSpec,
 	p.Name = "os"
 	p.Build = "name:build"
 	packages = append(packages, bpPackages...)
-	p.AddStage(osbuild.NewRPMStage(rpmStageOptions(repos), rpmStageInputs(packages)))
+	p.AddStage(osbuild.NewRPMStage(rpmStageOptions(repos), osbuild.NewRpmStageSourceFilesInputs(packages)))
 
 	// If the /boot is on a separate partition, the prefix for the BLS stage must be ""
 	if pt.BootPartition() == nil {
@@ -412,7 +411,7 @@ func ec2CommonPipelines(t *imageType, customizations *blueprint.Customizations,
 
 	treePipeline = prependKernelCmdlineStage(treePipeline, t, &partitionTable)
 	treePipeline.AddStage(osbuild.NewFSTabStage(partitionTable.FSTabStageOptionsV2()))
-	kernelVer := kernelVerStr(packageSetSpecs[blueprintPkgsKey], customizations.GetKernel().Name, t.Arch().Name())
+	kernelVer := rpmmd.GetVerStrFromPackageSpecListPanic(packageSetSpecs[blueprintPkgsKey], customizations.GetKernel().Name)
 	treePipeline.AddStage(bootloaderConfigStage(t, partitionTable, customizations.GetKernel(), kernelVer))
 	// The last stage must be the SELinux stage
 	treePipeline.AddStage(osbuild.NewSELinuxStage(selinuxStageOptions(false)))
@@ -553,7 +552,7 @@ func ec2SapPipelines(t *imageType, customizations *blueprint.Customizations, opt
 
 	treePipeline = prependKernelCmdlineStage(treePipeline, t, &partitionTable)
 	treePipeline.AddStage(osbuild.NewFSTabStage(partitionTable.FSTabStageOptionsV2()))
-	kernelVer := kernelVerStr(packageSetSpecs[blueprintPkgsKey], customizations.GetKernel().Name, t.Arch().Name())
+	kernelVer := rpmmd.GetVerStrFromPackageSpecListPanic(packageSetSpecs[blueprintPkgsKey], customizations.GetKernel().Name)
 	treePipeline.AddStage(bootloaderConfigStage(t, partitionTable, customizations.GetKernel(), kernelVer))
 	// The last stage must be the SELinux stage
 	treePipeline.AddStage(osbuild.NewSELinuxStage(selinuxStageOptions(false)))
@@ -622,7 +621,7 @@ func edgeInstallerPipelines(t *imageType, customizations *blueprint.Customizatio
 	pipelines := make([]osbuild.Pipeline, 0)
 	pipelines = append(pipelines, *buildPipeline(repos, packageSetSpecs[buildPkgsKey]))
 	installerPackages := packageSetSpecs[installerPkgsKey]
-	kernelVer := kernelVerStr(installerPackages, "kernel", t.Arch().Name())
+	kernelVer := rpmmd.GetVerStrFromPackageSpecListPanic(installerPackages, "kernel")
 	ostreeRepoPath := "/ostree/repo"
 	pipelines = append(pipelines, *anacondaTreePipeline(repos, installerPackages, kernelVer, t.Arch().Name(), ostreePayloadStages(options, ostreeRepoPath)))
 	pipelines = append(pipelines, *bootISOTreePipeline(kernelVer, t.Arch().Name(), ostreeKickstartStageOptions(fmt.Sprintf("file://%s", ostreeRepoPath), options.OSTree.Ref)))
@@ -707,7 +706,7 @@ func buildPipeline(repos []rpmmd.RepoConfig, buildPackageSpecs []rpmmd.PackageSp
 	p := new(osbuild.Pipeline)
 	p.Name = "build"
 	p.Runner = "org.osbuild.rhel90"
-	p.AddStage(osbuild.NewRPMStage(rpmStageOptions(repos), rpmStageInputs(buildPackageSpecs)))
+	p.AddStage(osbuild.NewRPMStage(rpmStageOptions(repos), osbuild.NewRpmStageSourceFilesInputs(buildPackageSpecs)))
 	p.AddStage(osbuild.NewSELinuxStage(selinuxStageOptions(true)))
 	return p
 }
@@ -717,7 +716,7 @@ func osPipeline(repos []rpmmd.RepoConfig, packages []rpmmd.PackageSpec, bpPackag
 	p.Name = "os"
 	p.Build = "name:build"
 	packages = append(packages, bpPackages...)
-	p.AddStage(osbuild.NewRPMStage(rpmStageOptions(repos), rpmStageInputs(packages)))
+	p.AddStage(osbuild.NewRPMStage(rpmStageOptions(repos), osbuild.NewRpmStageSourceFilesInputs(packages)))
 	p.AddStage(osbuild.NewFixBLSStage(&osbuild.FixBLSStageOptions{}))
 	language, keyboard := c.GetPrimaryLocale()
 	if language != nil {
@@ -798,7 +797,7 @@ func ostreeTreePipeline(repos []rpmmd.RepoConfig, packages []rpmmd.PackageSpec,
 	p.Build = "name:build"
 
 	packages = append(packages, bpPackages...)
-	p.AddStage(osbuild.NewRPMStage(rpmStageOptions(repos), rpmStageInputs(packages)))
+	p.AddStage(osbuild.NewRPMStage(rpmStageOptions(repos), osbuild.NewRpmStageSourceFilesInputs(packages)))
 	p.AddStage(osbuild.NewFixBLSStage(&osbuild.FixBLSStageOptions{}))
 	language, keyboard := c.GetPrimaryLocale()
 	if language != nil {
@@ -915,7 +914,7 @@ func containerTreePipeline(repos []rpmmd.RepoConfig, packages []rpmmd.PackageSpe
 	p := new(osbuild.Pipeline)
 	p.Name = "container-tree"
 	p.Build = "name:build"
-	p.AddStage(osbuild.NewRPMStage(rpmStageOptions(repos), rpmStageInputs(packages)))
+	p.AddStage(osbuild.NewRPMStage(rpmStageOptions(repos), osbuild.NewRpmStageSourceFilesInputs(packages)))
 	language, _ := c.GetPrimaryLocale()
 	if language != nil {
 		p.AddStage(osbuild.NewLocaleStage(&osbuild.LocaleStageOptions{Language: *language}))
@@ -926,7 +925,7 @@ func containerTreePipeline(repos []rpmmd.RepoConfig, packages []rpmmd.PackageSpe
 
 	p.AddStage(osbuild.NewOSTreePullStage(
 		&osbuild.OSTreePullStageOptions{Repo: "/var/www/html/repo"},
-		ostreePullStageInputs("org.osbuild.pipeline", "name:ostree-commit", options.OSTree.Ref),
+		osbuild.NewOstreePullStageInputs("org.osbuild.pipeline", "name:ostree-commit", options.OSTree.Ref),
 	))
 	return p
 }
@@ -959,7 +958,7 @@ func ostreePayloadStages(options distro.ImageOptions, ostreeRepoPath string) []*
 	stages = append(stages, osbuild.NewOSTreeInitStage(&osbuild.OSTreeInitStageOptions{Path: ostreeRepoPath}))
 	stages = append(stages, osbuild.NewOSTreePullStage(
 		&osbuild.OSTreePullStageOptions{Repo: ostreeRepoPath},
-		ostreePullStageInputs("org.osbuild.source", options.OSTree.Parent, options.OSTree.Ref),
+		osbuild.NewOstreePullStageInputs("org.osbuild.source", options.OSTree.Parent, options.OSTree.Ref),
 	))
 
 	return stages
@@ -969,7 +968,7 @@ func anacondaTreePipeline(repos []rpmmd.RepoConfig, packages []rpmmd.PackageSpec
 	p := new(osbuild.Pipeline)
 	p.Name = "anaconda-tree"
 	p.Build = "name:build"
-	p.AddStage(osbuild.NewRPMStage(rpmStageOptions(repos), rpmStageInputs(packages)))
+	p.AddStage(osbuild.NewRPMStage(rpmStageOptions(repos), osbuild.NewRpmStageSourceFilesInputs(packages)))
 	for _, stage := range payloadStages {
 		p.AddStage(stage)
 	}
@@ -1013,7 +1012,7 @@ func bootISOTreePipeline(kernelVer string, arch string, ksOptions *osbuild.Kicks
 	p.Name = "bootiso-tree"
 	p.Build = "name:build"
 
-	p.AddStage(osbuild.NewBootISOMonoStage(bootISOMonoStageOptions(kernelVer, arch), bootISOMonoStageInputs()))
+	p.AddStage(osbuild.NewBootISOMonoStage(bootISOMonoStageOptions(kernelVer, arch), osbuild.NewBootISOMonoStagePipelineTreeInputs("anaconda-tree")))
 	p.AddStage(osbuild.NewKickstartStage(ksOptions))
 	p.AddStage(osbuild.NewDiscinfoStage(discinfoStageOptions(arch)))
 
@@ -1024,7 +1023,7 @@ func bootISOPipeline(filename string, arch string) *osbuild.Pipeline {
 	p.Name = "bootiso"
 	p.Build = "name:build"
 
-	p.AddStage(osbuild.NewXorrisofsStage(xorrisofsStageOptions(filename, arch), xorrisofsStageInputs()))
+	p.AddStage(osbuild.NewXorrisofsStage(xorrisofsStageOptions(filename, arch), osbuild.NewXorrisofsStagePipelineTreeInputs("bootiso-tree")))
 	p.AddStage(osbuild.NewImplantisomd5Stage(&osbuild.Implantisomd5StageOptions{Filename: filename}))
 
 	return p
@@ -1046,7 +1045,7 @@ func liveImagePipeline(inputPipelineName string, outputFilename string, pt *disk
 
 	inputName := "root-tree"
 	copyOptions, copyDevices, copyMounts := copyFSTreeOptions(inputName, inputPipelineName, pt, loopback)
-	copyInputs := copyPipelineTreeInputs(inputName, inputPipelineName)
+	copyInputs := osbuild.NewCopyStagePipelineTreeInputs(inputName, inputPipelineName)
 	p.AddStage(osbuild.NewCopyStage(copyOptions, copyInputs, copyDevices, copyMounts))
 	p.AddStage(bootloaderInstStage(outputFilename, pt, arch, kernelVer, copyDevices, copyMounts, loopback))
 	return p
@@ -1067,8 +1066,8 @@ func xzArchivePipeline(inputPipelineName, inputFilename, outputFilename string)
 
 // mkfsStages generates a list of org.osbuild.mkfs.* stages based on a
 // partition table description for a single device node
-func mkfsStages(pt *disk.PartitionTable, device *osbuild.Device) []*osbuild2.Stage {
-	stages := make([]*osbuild2.Stage, 0, len(pt.Partitions))
+func mkfsStages(pt *disk.PartitionTable, device *osbuild.Device) []*osbuild.Stage {
+	stages := make([]*osbuild.Stage, 0, len(pt.Partitions))
 
 	// assume loopback device for simplicity since it's the only one currently supported
 	// panic if the conversion fails
@@ -1127,7 +1126,7 @@ func qemuPipeline(inputPipelineName, inputFilename, outputFilename, format, qcow
 	p.Name = format
 	p.Build = "name:build"
 
-	qemuStage := osbuild.NewQEMUStage(qemuStageOptions(outputFilename, format, qcow2Compat), qemuStageInputs(inputPipelineName, inputFilename))
+	qemuStage := osbuild.NewQEMUStage(qemuStageOptions(outputFilename, format, qcow2Compat), osbuild.NewQemuStagePipelineFilesInputs(inputPipelineName, inputFilename))
 	p.AddStage(qemuStage)
 	return p
 }
@@ -1155,19 +1154,3 @@ func bootloaderInstStage(filename string, pt *disk.PartitionTable, arch *archite
 
 	return nil
 }
-
-func kernelVerStr(pkgs []rpmmd.PackageSpec, kernelName, arch string) string {
-	kernelPkg := new(rpmmd.PackageSpec)
-	for _, pkg := range pkgs {
-		if pkg.Name == kernelName {
-			// Implicit memory alasing doesn't couse any bug in this case
-			/* #nosec G601 */
-			kernelPkg = &pkg
-			break
-		}
-	}
-	if kernelPkg == nil {
-		panic(fmt.Sprintf("kernel package %q not found", kernelName))
-	}
-	return fmt.Sprintf("%s-%s.%s", kernelPkg.Version, kernelPkg.Release, kernelPkg.Arch)
-}
diff --git a/internal/distro/rhel90beta/stage_inputs.go b/internal/distro/rhel90beta/stage_inputs.go
deleted file mode 100644
index 19ca6e8d9..000000000
--- a/internal/distro/rhel90beta/stage_inputs.go
+++ /dev/null
@@ -1,74 +0,0 @@
-package rhel90beta
-
-import (
-	osbuild "github.com/osbuild/osbuild-composer/internal/osbuild2"
-	"github.com/osbuild/osbuild-composer/internal/rpmmd"
-)
-
-func bootISOMonoStageInputs() *osbuild.BootISOMonoStageInputs {
-	rootfsInput := new(osbuild.BootISOMonoStageInput)
-	rootfsInput.Type = "org.osbuild.tree"
-	rootfsInput.Origin = "org.osbuild.pipeline"
-	rootfsInput.References = osbuild.BootISOMonoStageReferences{"name:anaconda-tree"}
-	return &osbuild.BootISOMonoStageInputs{
-		RootFS: rootfsInput,
-	}
-}
-
-func rpmStageInputs(specs []rpmmd.PackageSpec) *osbuild.RPMStageInputs {
-	stageInput := new(osbuild.RPMStageInput)
-	stageInput.Type = "org.osbuild.files"
-	stageInput.Origin = "org.osbuild.source"
-	stageInput.References = pkgRefs(specs)
-	return &osbuild.RPMStageInputs{Packages: stageInput}
-}
-
-func pkgRefs(specs []rpmmd.PackageSpec) osbuild.RPMStageReferences {
-	refs := make([]string, len(specs))
-	for idx, pkg := range specs {
-		refs[idx] = pkg.Checksum
-	}
-	return refs
-}
-
-func ostreePullStageInputs(origin, source, commitRef string) *osbuild.OSTreePullStageInputs {
-	pullStageInput := new(osbuild.OSTreePullStageInput)
-	pullStageInput.Type = "org.osbuild.ostree"
-	pullStageInput.Origin = origin
-
-	inputRefs := make(map[string]osbuild.OSTreePullStageReference)
-	inputRefs[source] = osbuild.OSTreePullStageReference{Ref: commitRef}
-	pullStageInput.References = inputRefs
-	return &osbuild.OSTreePullStageInputs{Commits: pullStageInput}
-}
-
-func xorrisofsStageInputs() *osbuild.XorrisofsStageInputs {
-	input := new(osbuild.XorrisofsStageInput)
-	input.Type = "org.osbuild.tree"
-	input.Origin = "org.osbuild.pipeline"
-	input.References = osbuild.XorrisofsStageReferences{"name:bootiso-tree"}
-	return &osbuild.XorrisofsStageInputs{Tree: input}
-}
-
-func copyPipelineTreeInputs(name, inputPipeline string) *osbuild.CopyStageInputs {
-	inputName := "root-tree"
-	treeInput := osbuild.CopyStageInput{}
-	treeInput.Type = "org.osbuild.tree"
-	treeInput.Origin = "org.osbuild.pipeline"
-	treeInput.References = []string{"name:" + inputPipeline}
-	return &osbuild.CopyStageInputs{inputName: treeInput}
-}
-
-func qemuStageInputs(stage, file string) *osbuild.QEMUStageInputs {
-	stageKey := "name:" + stage
-	ref := map[string]osbuild.QEMUFile{
-		stageKey: {
-			File: file,
-		},
-	}
-	input := new(osbuild.QEMUStageInput)
-	input.Type = "org.osbuild.files"
-	input.Origin = "org.osbuild.pipeline"
-	input.References = ref
-	return &osbuild.QEMUStageInputs{Image: input}
-}
diff --git a/internal/distro/rhel90beta/stage_options.go b/internal/distro/rhel90beta/stage_options.go
index 46758f1bb..9da2960c6 100644
--- a/internal/distro/rhel90beta/stage_options.go
+++ b/internal/distro/rhel90beta/stage_options.go
@@ -505,10 +505,3 @@ func qemuStageOptions(filename, format, compat string) *osbuild.QEMUStageOptions
 		Format:   options,
 	}
 }
-
-func kernelCmdlineStageOptions(rootUUID string, kernelOptions string) *osbuild.KernelCmdlineStageOptions {
-	return &osbuild.KernelCmdlineStageOptions{
-		RootFsUUID: rootUUID,
-		KernelOpts: kernelOptions,
-	}
-}
diff --git a/internal/jobqueue/fsjobqueue/fsjobqueue.go b/internal/jobqueue/fsjobqueue/fsjobqueue.go
index cd900e028..949c7b256 100644
--- a/internal/jobqueue/fsjobqueue/fsjobqueue.go
+++ b/internal/jobqueue/fsjobqueue/fsjobqueue.go
@@ -12,11 +12,10 @@
 package fsjobqueue
 
 import (
+	"container/list"
 	"context"
 	"encoding/json"
-	"errors"
 	"fmt"
-	"reflect"
 	"sync"
 	"time"
 
@@ -35,8 +34,11 @@ type fsJobQueue struct {
 
 	db *jsondb.JSONDatabase
 
-	// Maps job types to channels of job ids for that type.
-	pending map[string]chan uuid.UUID
+	// List of pending job
+	pending *list.List
+
+	// Set of goroutines waiting for new pending jobs
+	listeners map[chan struct{}]struct{}
 
 	// Maps job ids to the jobs that depend on it, if any of those
 	// dependants have not yet finished.
@@ -73,20 +75,17 @@ type job struct {
 	Canceled bool `json:"canceled,omitempty"`
 }
 
-// The size of channels used in fsJobQueue for queueing jobs.
-// Note that each job type has its own queue.
-const channelSize = 100
-
 // Create a new fsJobQueue object for `dir`. This object must have exclusive
 // access to `dir`. If `dir` contains jobs created from previous runs, they are
 // loaded and rescheduled to run if necessary.
 func New(dir string) (*fsJobQueue, error) {
 	q := &fsJobQueue{
 		db:           jsondb.New(dir, 0600),
-		pending:      make(map[string]chan uuid.UUID),
+		pending:      list.New(),
 		dependants:   make(map[uuid.UUID][]uuid.UUID),
 		jobIdByToken: make(map[uuid.UUID]uuid.UUID),
 		heartbeats:   make(map[uuid.UUID]time.Time),
+		listeners:    make(map[chan struct{}]struct{}),
 	}
 
 	// Look for jobs that are still pending and build the dependant map.
@@ -182,43 +181,35 @@ func (q *fsJobQueue) Dequeue(ctx context.Context, jobTypes []string) (uuid.UUID,
 		return uuid.Nil, uuid.Nil, nil, "", nil, jobqueue.ErrDequeueTimeout
 	}
 
-	// Filter q.pending by the `jobTypes`. Ignore those job types that this
-	// queue doesn't accept.
-	chans := []chan uuid.UUID{}
-	for _, jt := range jobTypes {
-		c, exists := q.pending[jt]
-		if !exists {
-			c = make(chan uuid.UUID, channelSize)
-			q.pending[jt] = c
-		}
-		chans = append(chans, c)
-	}
+	// Add a new listener
+	c := make(chan struct{}, 1)
+	q.listeners[c] = struct{}{}
+	defer delete(q.listeners, c)
 
-	// Loop until finding a non-canceled job and pending.
+	// Loop until finding a suitable job
 	var j *job
 	for {
-		// Unlock the mutex while polling channels, so that multiple goroutines
-		// can wait at the same time.
-		q.mu.Unlock()
-		id, err := selectUUIDChannel(ctx, chans)
-		q.mu.Lock()
-
+		var found bool
+		var err error
+		j, found, err = q.dequeueSuitableJob(jobTypes)
 		if err != nil {
-			if errors.As(err, &context.Canceled) || errors.As(err, &context.DeadlineExceeded) {
-				return uuid.Nil, uuid.Nil, nil, "", nil, jobqueue.ErrDequeueTimeout
-			}
 			return uuid.Nil, uuid.Nil, nil, "", nil, err
 		}
-
-		j, err = q.readJob(id)
-		if err != nil {
-			return uuid.Nil, uuid.Nil, nil, "", nil, err
+		if found {
+			break
 		}
 
-		// jobs must be non-cancelled and pending
-		if !j.Canceled && j.StartedAt.IsZero() {
-			break
+		// Unlock the mutex while polling channels, so that multiple goroutines
+		// can wait at the same time.
+		q.mu.Unlock()
+		select {
+		case <-c:
+		case <-ctx.Done():
+			// there's defer q.mu.Unlock(), so let's lock
+			q.mu.Lock()
+			return uuid.Nil, uuid.Nil, nil, "", nil, jobqueue.ErrDequeueTimeout
 		}
+		q.mu.Lock()
 	}
 
 	j.StartedAt = time.Now()
@@ -256,6 +247,8 @@ func (q *fsJobQueue) DequeueByID(ctx context.Context, id uuid.UUID) (uuid.UUID,
 		return uuid.Nil, nil, "", nil, jobqueue.ErrNotPending
 	}
 
+	q.removePendingJob(id)
+
 	j.StartedAt = time.Now()
 
 	j.Token = uuid.New()
@@ -331,6 +324,11 @@ func (q *fsJobQueue) CancelJob(id uuid.UUID) error {
 		return jobqueue.ErrNotRunning
 	}
 
+	// if the cancelled job is pending, remove it from the list
+	if j.StartedAt.IsZero() {
+		q.removePendingJob(id)
+	}
+
 	j.Canceled = true
 
 	delete(q.heartbeats, j.Token)
@@ -436,12 +434,16 @@ func (q *fsJobQueue) maybeEnqueue(j *job, updateDependants bool) error {
 	}
 
 	if depsFinished {
-		c, exists := q.pending[j.Type]
-		if !exists {
-			c = make(chan uuid.UUID, channelSize)
-			q.pending[j.Type] = c
+		// add the job to the list of pending ones
+		q.pending.PushBack(j.Id)
+
+		// notify all listeners in a non-blocking way
+		for c := range q.listeners {
+			select {
+			case c <- struct{}{}:
+			default:
+			}
 		}
-		c <- j.Id
 	} else if updateDependants {
 		for _, id := range j.Dependencies {
 			q.dependants[id] = append(q.dependants[id], j.Id)
@@ -468,33 +470,69 @@ func (q *fsJobQueue) hasAllFinishedDependencies(j *job) (bool, error) {
 	return true, nil
 }
 
-// Select on a list of `chan uuid.UUID`s. Returns an error if one of the
-// channels is closed.
+// dequeueSuitableJob finds a suitable job in the list of pending jobs, removes it from there and returns it
+//
+// The job must meet the following conditions:
+// - must be pending
+// - its dependencies must be finished
+// - must be of one of the type from jobTypes
+//
+// If a suitable job is not found, false is returned.
+// If an error occurs during the search, it's returned.
+func (q *fsJobQueue) dequeueSuitableJob(jobTypes []string) (*job, bool, error) {
+	el := q.pending.Front()
+	for el != nil {
+		id := el.Value.(uuid.UUID)
+
+		j, err := q.readJob(id)
+		if err != nil {
+			return nil, false, err
+		}
+
+		if !jobMatchesCriteria(j, jobTypes) {
+			el = el.Next()
+			continue
+		}
+
+		ready, err := q.hasAllFinishedDependencies(j)
+		if err != nil {
+			return nil, false, err
+		}
+		if ready {
+			q.pending.Remove(el)
+			return j, true, nil
+		}
+		el = el.Next()
+	}
+
+	return nil, false, nil
+}
+
+// removePendingJob removes a job with given ID from the list of pending jobs
+//
+// If the job isn't in the list, this is no-op.
+func (q *fsJobQueue) removePendingJob(id uuid.UUID) {
+	el := q.pending.Front()
+	for el != nil {
+		if el.Value.(uuid.UUID) == id {
+			q.pending.Remove(el)
+			return
+		}
+
+		el = el.Next()
+	}
+}
+
+// jobMatchesCriteria returns true if it matches criteria defined in parameters
 //
-// Uses reflect.Select(), because the `select` statement cannot operate on an
-// unknown amount of channels.
-func selectUUIDChannel(ctx context.Context, chans []chan uuid.UUID) (uuid.UUID, error) {
-	cases := []reflect.SelectCase{
-		{
-			Dir:  reflect.SelectRecv,
-			Chan: reflect.ValueOf(ctx.Done()),
-		},
-	}
-	for _, c := range chans {
-		cases = append(cases, reflect.SelectCase{
-			Dir:  reflect.SelectRecv,
-			Chan: reflect.ValueOf(c),
-		})
-	}
-
-	chosen, value, recvOK := reflect.Select(cases)
-	if !recvOK {
-		if chosen == 0 {
-			return uuid.Nil, ctx.Err()
-		} else {
-			return uuid.Nil, errors.New("channel was closed unexpectedly")
+// Criteria:
+//  - the job's type is one of the acceptedJobTypes
+func jobMatchesCriteria(j *job, acceptedJobTypes []string) bool {
+	for _, jt := range acceptedJobTypes {
+		if jt == j.Type {
+			return true
 		}
 	}
 
-	return value.Interface().(uuid.UUID), nil
+	return false
 }
diff --git a/internal/osbuild2/bootiso_stage.go b/internal/osbuild2/bootiso_stage.go
index 6420d8bdf..7c4f5f6d6 100644
--- a/internal/osbuild2/bootiso_stage.go
+++ b/internal/osbuild2/bootiso_stage.go
@@ -87,3 +87,13 @@ func NewBootISOMonoStage(options *BootISOMonoStageOptions, inputs *BootISOMonoSt
 		Inputs:  inputs,
 	}
 }
+
+func NewBootISOMonoStagePipelineTreeInputs(pipeline string) *BootISOMonoStageInputs {
+	rootfsInput := new(BootISOMonoStageInput)
+	rootfsInput.Type = "org.osbuild.tree"
+	rootfsInput.Origin = "org.osbuild.pipeline"
+	rootfsInput.References = BootISOMonoStageReferences{"name:" + pipeline}
+	return &BootISOMonoStageInputs{
+		RootFS: rootfsInput,
+	}
+}
diff --git a/internal/osbuild2/copy_stage.go b/internal/osbuild2/copy_stage.go
index 510ea3a67..8b8917f98 100644
--- a/internal/osbuild2/copy_stage.go
+++ b/internal/osbuild2/copy_stage.go
@@ -58,3 +58,11 @@ func NewCopyStageSimple(options *CopyStageOptions, inputs CopyStageInputsNew) *S
 		Inputs:  stageInputs,
 	}
 }
+
+func NewCopyStagePipelineTreeInputs(inputName, inputPipeline string) *CopyStageInputs {
+	treeInput := CopyStageInput{}
+	treeInput.Type = "org.osbuild.tree"
+	treeInput.Origin = "org.osbuild.pipeline"
+	treeInput.References = []string{"name:" + inputPipeline}
+	return &CopyStageInputs{inputName: treeInput}
+}
diff --git a/internal/osbuild2/kernel_cmdline_stage.go b/internal/osbuild2/kernel_cmdline_stage.go
index c75624486..45e5d791d 100644
--- a/internal/osbuild2/kernel_cmdline_stage.go
+++ b/internal/osbuild2/kernel_cmdline_stage.go
@@ -17,3 +17,10 @@ func NewKernelCmdlineStage(options *KernelCmdlineStageOptions) *Stage {
 		Options: options,
 	}
 }
+
+func NewKernelCmdlineStageOptions(rootUUID string, kernelOptions string) *KernelCmdlineStageOptions {
+	return &KernelCmdlineStageOptions{
+		RootFsUUID: rootUUID,
+		KernelOpts: kernelOptions,
+	}
+}
diff --git a/internal/osbuild2/ostree_pull_stage.go b/internal/osbuild2/ostree_pull_stage.go
index 048a4fa01..a2a762ae5 100644
--- a/internal/osbuild2/ostree_pull_stage.go
+++ b/internal/osbuild2/ostree_pull_stage.go
@@ -39,3 +39,14 @@ func NewOSTreePullStage(options *OSTreePullStageOptions, inputs *OSTreePullStage
 		Options: options,
 	}
 }
+
+func NewOstreePullStageInputs(origin, source, commitRef string) *OSTreePullStageInputs {
+	pullStageInput := new(OSTreePullStageInput)
+	pullStageInput.Type = "org.osbuild.ostree"
+	pullStageInput.Origin = origin
+
+	inputRefs := make(map[string]OSTreePullStageReference)
+	inputRefs[source] = OSTreePullStageReference{Ref: commitRef}
+	pullStageInput.References = inputRefs
+	return &OSTreePullStageInputs{Commits: pullStageInput}
+}
diff --git a/internal/osbuild2/qemu_stage.go b/internal/osbuild2/qemu_stage.go
index be863cdbe..8093babe5 100644
--- a/internal/osbuild2/qemu_stage.go
+++ b/internal/osbuild2/qemu_stage.go
@@ -105,3 +105,17 @@ func (options QEMUStageOptions) MarshalJSON() ([]byte, error) {
 
 	return json.Marshal(qemuStageOptions(options))
 }
+
+func NewQemuStagePipelineFilesInputs(stage, file string) *QEMUStageInputs {
+	stageKey := "name:" + stage
+	ref := map[string]QEMUFile{
+		stageKey: {
+			File: file,
+		},
+	}
+	input := new(QEMUStageInput)
+	input.Type = "org.osbuild.files"
+	input.Origin = "org.osbuild.pipeline"
+	input.References = ref
+	return &QEMUStageInputs{Image: input}
+}
diff --git a/internal/osbuild2/rpm_stage.go b/internal/osbuild2/rpm_stage.go
index ee2ef2d00..59b9bea40 100644
--- a/internal/osbuild2/rpm_stage.go
+++ b/internal/osbuild2/rpm_stage.go
@@ -1,5 +1,9 @@
 package osbuild2
 
+import (
+	"github.com/osbuild/osbuild-composer/internal/rpmmd"
+)
+
 type RPMStageOptions struct {
 	// Array of GPG key contents to import
 	GPGKeys []string `json:"gpgkeys,omitempty"`
@@ -70,3 +74,52 @@ type RPMPackageMetadata struct {
 }
 
 func (RPMStageMetadata) isStageMetadata() {}
+
+func OSBuildMetadataToRPMs(stagesMetadata map[string]StageMetadata) []rpmmd.RPM {
+	rpms := make([]rpmmd.RPM, 0)
+	for _, md := range stagesMetadata {
+		switch metadata := md.(type) {
+		case *RPMStageMetadata:
+			for _, pkg := range metadata.Packages {
+				rpms = append(rpms, rpmmd.RPM{
+					Type:      "rpm",
+					Name:      pkg.Name,
+					Epoch:     pkg.Epoch,
+					Version:   pkg.Version,
+					Release:   pkg.Release,
+					Arch:      pkg.Arch,
+					Sigmd5:    pkg.SigMD5,
+					Signature: RPMPackageMetadataToSignature(pkg),
+				})
+			}
+		default:
+			continue
+		}
+	}
+	return rpms
+}
+
+func RPMPackageMetadataToSignature(pkg RPMPackageMetadata) *string {
+	if pkg.SigGPG != "" {
+		return &pkg.SigGPG
+	} else if pkg.SigPGP != "" {
+		return &pkg.SigPGP
+	}
+	return nil
+}
+
+func NewRpmStageSourceFilesInputs(specs []rpmmd.PackageSpec) *RPMStageInputs {
+	stageInput := new(RPMStageInput)
+	stageInput.Type = "org.osbuild.files"
+	stageInput.Origin = "org.osbuild.source"
+	stageInput.References = pkgRefs(specs)
+	return &RPMStageInputs{Packages: stageInput}
+}
+
+func pkgRefs(specs []rpmmd.PackageSpec) RPMStageReferences {
+	refs := make([]string, len(specs))
+	for idx, pkg := range specs {
+		refs[idx] = pkg.Checksum
+	}
+	return refs
+}
diff --git a/internal/osbuild2/xorrisofs_stage.go b/internal/osbuild2/xorrisofs_stage.go
index 99724061f..275168a28 100644
--- a/internal/osbuild2/xorrisofs_stage.go
+++ b/internal/osbuild2/xorrisofs_stage.go
@@ -55,3 +55,11 @@ func NewXorrisofsStage(options *XorrisofsStageOptions, inputs *XorrisofsStageInp
 		Inputs:  inputs,
 	}
 }
+
+func NewXorrisofsStagePipelineTreeInputs(pipeline string) *XorrisofsStageInputs {
+	input := new(XorrisofsStageInput)
+	input.Type = "org.osbuild.tree"
+	input.Origin = "org.osbuild.pipeline"
+	input.References = XorrisofsStageReferences{"name:" + pipeline}
+	return &XorrisofsStageInputs{Tree: input}
+}
diff --git a/internal/rpmmd/metadata.go b/internal/rpmmd/metadata.go
index e0bfd9820..5ea531be3 100644
--- a/internal/rpmmd/metadata.go
+++ b/internal/rpmmd/metadata.go
@@ -2,8 +2,6 @@ package rpmmd
 
 import (
 	"fmt"
-
-	osbuild "github.com/osbuild/osbuild-composer/internal/osbuild2"
 )
 
 type RPM struct {
@@ -26,39 +24,6 @@ func (r RPM) String() string {
 	return fmt.Sprintf("%s-%s%s-%s.%s", r.Name, epoch, r.Version, r.Release, r.Arch)
 }
 
-func OSBuildMetadataToRPMs(stagesMetadata map[string]osbuild.StageMetadata) []RPM {
-	rpms := make([]RPM, 0)
-	for _, md := range stagesMetadata {
-		switch metadata := md.(type) {
-		case *osbuild.RPMStageMetadata:
-			for _, pkg := range metadata.Packages {
-				rpms = append(rpms, RPM{
-					Type:      "rpm",
-					Name:      pkg.Name,
-					Epoch:     pkg.Epoch,
-					Version:   pkg.Version,
-					Release:   pkg.Release,
-					Arch:      pkg.Arch,
-					Sigmd5:    pkg.SigMD5,
-					Signature: PackageMetadataToSignature(pkg),
-				})
-			}
-		default:
-			continue
-		}
-	}
-	return rpms
-}
-
-func PackageMetadataToSignature(pkg osbuild.RPMPackageMetadata) *string {
-	if pkg.SigGPG != "" {
-		return &pkg.SigGPG
-	} else if pkg.SigPGP != "" {
-		return &pkg.SigPGP
-	}
-	return nil
-}
-
 // Deduplicate a list of RPMs based on NEVRA string
 func DeduplicateRPMs(rpms []RPM) []RPM {
 	rpmMap := make(map[string]struct{}, len(rpms))
diff --git a/internal/rpmmd/repository.go b/internal/rpmmd/repository.go
index 1f87ac431..ff0acb841 100644
--- a/internal/rpmmd/repository.go
+++ b/internal/rpmmd/repository.go
@@ -203,6 +203,23 @@ func (re *RepositoryError) Error() string {
 	return re.msg
 }
 
+func GetVerStrFromPackageSpecList(pkgs []PackageSpec, packageName string) (string, error) {
+	for _, pkg := range pkgs {
+		if pkg.Name == packageName {
+			return fmt.Sprintf("%s-%s.%s", pkg.Version, pkg.Release, pkg.Arch), nil
+		}
+	}
+	return "", fmt.Errorf("package %q not found in the PackageSpec list", packageName)
+}
+
+func GetVerStrFromPackageSpecListPanic(pkgs []PackageSpec, packageName string) string {
+	pkgVerStr, err := GetVerStrFromPackageSpecList(pkgs, packageName)
+	if err != nil {
+		panic(err)
+	}
+	return pkgVerStr
+}
+
 func loadRepositoriesFromFile(filename string) (map[string][]RepoConfig, error) {
 	f, err := os.Open(filename)
 	if err != nil {
diff --git a/schutzbot/deploy.sh b/schutzbot/deploy.sh
index 5fc98d544..5fa89c3cc 100755
--- a/schutzbot/deploy.sh
+++ b/schutzbot/deploy.sh
@@ -126,6 +126,9 @@ retry sudo dnf -y upgrade selinux-policy
 # Note: installing only -tests to catch missing dependencies
 retry sudo dnf -y install "${PROJECT}-tests"
 
+# Save osbuild-composer NVR to a file to be used as CI artifact
+rpm -q osbuild-composer > COMPOSER_NVR
+
 if [ "${NIGHTLY:=false}" == "true" ]; then
     # check if we've installed the osbuild-composer RPM from the nightly tree
     # under test or happen to install a newer version from one of the S3 repositories
diff --git a/schutzbot/slack_notification.sh b/schutzbot/slack_notification.sh
index 9e50600fa..2fc9a71fc 100755
--- a/schutzbot/slack_notification.sh
+++ b/schutzbot/slack_notification.sh
@@ -3,7 +3,8 @@
 set -eux
 
 COMPOSE_ID=$(cat COMPOSE_ID)
-MESSAGE="\"Nightly pipeline execution on *$COMPOSE_ID* finished with status *$1* $2 \n QE: @atodorov, @jrusz, @jabia \n Link to results: $CI_PIPELINE_URL \""
+COMPOSER_NVR=$(cat COMPOSER_NVR)
+MESSAGE="\"Nightly pipeline execution on *$COMPOSE_ID* with *$COMPOSER_NVR* finished with status *$1* $2 \n QE: @atodorov, @jrusz, @jabia \n Link to results: $CI_PIPELINE_URL \""
 
 curl \
     -X POST \
diff --git a/templates/packer/composer.pkr.hcl b/templates/packer/composer.pkr.hcl
index a37af61e6..850cf6e65 100644
--- a/templates/packer/composer.pkr.hcl
+++ b/templates/packer/composer.pkr.hcl
@@ -16,7 +16,7 @@ source "amazon-ebs" "image_builder" {
   launch_block_device_mappings {
     delete_on_termination = "true"
     device_name           = "/dev/sda1"
-    volume_size           = 25
+    volume_size           = 10
     volume_type           = "gp2"
   }
 


Test patch:
diff --git a/internal/distro/distro_test_common/distro_test_common.go b/internal/distro/distro_test_common/distro_test_common.go
index 457b0aac9..009ff10f7 100644
--- a/internal/distro/distro_test_common/distro_test_common.go
+++ b/internal/distro/distro_test_common/distro_test_common.go
@@ -190,3 +190,32 @@ func TestDistro_KernelOption(t *testing.T, d distro.Distro) {
 		}
 	}
 }
+
+// GetTestingPackageSpecSets returns PackageSpecSets useful for unit testing.
+//
+// A dummy PackageSpec for the provided packageName is added
+// to all PackageSpecSets provided in pkgSetNames.
+//
+// E.g. `kernel` package is a hard requirement of some payload pipelines
+// and they panic if it is not found in the packageSpecSets passed to
+// Manifest().
+func GetTestingPackageSpecSets(packageName, arch string, pkgSetNames []string) map[string][]rpmmd.PackageSpec {
+	pkgTestingSpec := []rpmmd.PackageSpec{
+		{
+			Name:           packageName,
+			Epoch:          0,
+			Version:        "1.2.3",
+			Release:        "2.el123",
+			Arch:           arch,
+			RemoteLocation: "http://example.org",
+			Checksum:       "lorenipsum",
+			Secrets:        "lorenipsum",
+			CheckGPG:       false,
+		},
+	}
+	testPackageSpecSets := map[string][]rpmmd.PackageSpec{}
+	for _, pkgSetName := range pkgSetNames {
+		testPackageSpecSets[pkgSetName] = pkgTestingSpec
+	}
+	return testPackageSpecSets
+}
diff --git a/internal/distro/rhel85/distro_test.go b/internal/distro/rhel85/distro_test.go
index b9fde81e9..ab1cecd2a 100644
--- a/internal/distro/rhel85/distro_test.go
+++ b/internal/distro/rhel85/distro_test.go
@@ -415,7 +415,8 @@ func TestDistro_ManifestError(t *testing.T) {
 			imgOpts := distro.ImageOptions{
 				Size: imgType.Size(0),
 			}
-			_, err := imgType.Manifest(bp.Customizations, imgOpts, nil, nil, 0)
+			testPackageSpecSets := distro_test_common.GetTestingPackageSpecSets("kernel", arch.Name(), imgType.PayloadPackageSets())
+			_, err := imgType.Manifest(bp.Customizations, imgOpts, nil, testPackageSpecSets, 0)
 			if imgTypeName == "edge-commit" || imgTypeName == "edge-container" {
 				assert.EqualError(t, err, "kernel boot parameter customizations are not supported for ostree types")
 			} else if imgTypeName == "edge-raw-image" {
@@ -580,7 +581,8 @@ func TestDistro_CustomFileSystemManifestError(t *testing.T) {
 		arch, _ := r8distro.GetArch(archName)
 		for _, imgTypeName := range arch.ListImageTypes() {
 			imgType, _ := arch.GetImageType(imgTypeName)
-			_, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, nil, 0)
+			testPackageSpecSets := distro_test_common.GetTestingPackageSpecSets("kernel", arch.Name(), imgType.PayloadPackageSets())
+			_, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, testPackageSpecSets, 0)
 			if imgTypeName == "edge-commit" || imgTypeName == "edge-container" {
 				assert.EqualError(t, err, "Custom mountpoints are not supported for ostree types")
 			} else if imgTypeName == "edge-installer" || imgTypeName == "edge-simplified-installer" || imgTypeName == "edge-raw-image" {
@@ -608,7 +610,8 @@ func TestDistro_TestRootMountPoint(t *testing.T) {
 		arch, _ := r8distro.GetArch(archName)
 		for _, imgTypeName := range arch.ListImageTypes() {
 			imgType, _ := arch.GetImageType(imgTypeName)
-			_, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, nil, 0)
+			testPackageSpecSets := distro_test_common.GetTestingPackageSpecSets("kernel", arch.Name(), imgType.PayloadPackageSets())
+			_, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, testPackageSpecSets, 0)
 			if imgTypeName == "edge-commit" || imgTypeName == "edge-container" {
 				assert.EqualError(t, err, "Custom mountpoints are not supported for ostree types")
 			} else if imgTypeName == "edge-installer" || imgTypeName == "edge-simplified-installer" || imgTypeName == "edge-raw-image" {
@@ -640,7 +643,8 @@ func TestDistro_CustomFileSystemSubDirectories(t *testing.T) {
 		arch, _ := r8distro.GetArch(archName)
 		for _, imgTypeName := range arch.ListImageTypes() {
 			imgType, _ := arch.GetImageType(imgTypeName)
-			_, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, nil, 0)
+			testPackageSpecSets := distro_test_common.GetTestingPackageSpecSets("kernel", arch.Name(), imgType.PayloadPackageSets())
+			_, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, testPackageSpecSets, 0)
 			if strings.HasPrefix(imgTypeName, "edge-") {
 				continue
 			} else {
@@ -678,7 +682,8 @@ func TestDistro_MountpointsWithArbitraryDepthAllowed(t *testing.T) {
 		arch, _ := r8distro.GetArch(archName)
 		for _, imgTypeName := range arch.ListImageTypes() {
 			imgType, _ := arch.GetImageType(imgTypeName)
-			_, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, nil, 0)
+			testPackageSpecSets := distro_test_common.GetTestingPackageSpecSets("kernel", arch.Name(), imgType.PayloadPackageSets())
+			_, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, testPackageSpecSets, 0)
 			if strings.HasPrefix(imgTypeName, "edge-") {
 				continue
 			} else {
@@ -712,7 +717,8 @@ func TestDistro_DirtyMountpointsNotAllowed(t *testing.T) {
 		arch, _ := r8distro.GetArch(archName)
 		for _, imgTypeName := range arch.ListImageTypes() {
 			imgType, _ := arch.GetImageType(imgTypeName)
-			_, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, nil, 0)
+			testPackageSpecSets := distro_test_common.GetTestingPackageSpecSets("kernel", arch.Name(), imgType.PayloadPackageSets())
+			_, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, testPackageSpecSets, 0)
 			if strings.HasPrefix(imgTypeName, "edge-") {
 				continue
 			} else {
@@ -742,7 +748,8 @@ func TestDistro_CustomFileSystemPatternMatching(t *testing.T) {
 		arch, _ := r8distro.GetArch(archName)
 		for _, imgTypeName := range arch.ListImageTypes() {
 			imgType, _ := arch.GetImageType(imgTypeName)
-			_, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, nil, 0)
+			testPackageSpecSets := distro_test_common.GetTestingPackageSpecSets("kernel", arch.Name(), imgType.PayloadPackageSets())
+			_, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, testPackageSpecSets, 0)
 			if imgTypeName == "edge-commit" || imgTypeName == "edge-container" {
 				assert.EqualError(t, err, "Custom mountpoints are not supported for ostree types")
 			} else if imgTypeName == "edge-installer" || imgTypeName == "edge-simplified-installer" || imgTypeName == "edge-raw-image" {
@@ -770,7 +777,8 @@ func TestDistro_CustomUsrPartitionNotLargeEnough(t *testing.T) {
 		arch, _ := r8distro.GetArch(archName)
 		for _, imgTypeName := range arch.ListImageTypes() {
 			imgType, _ := arch.GetImageType(imgTypeName)
-			_, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, nil, 0)
+			testPackageSpecSets := distro_test_common.GetTestingPackageSpecSets("kernel", arch.Name(), imgType.PayloadPackageSets())
+			_, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, testPackageSpecSets, 0)
 			if imgTypeName == "edge-commit" || imgTypeName == "edge-container" {
 				assert.EqualError(t, err, "Custom mountpoints are not supported for ostree types")
 			} else if imgTypeName == "edge-installer" || imgTypeName == "edge-simplified-installer" || imgTypeName == "edge-raw-image" {
diff --git a/internal/distro/rhel86/distro_test.go b/internal/distro/rhel86/distro_test.go
index 0dd25fe9a..69f4ccb9d 100644
--- a/internal/distro/rhel86/distro_test.go
+++ b/internal/distro/rhel86/distro_test.go
@@ -424,7 +424,8 @@ func TestDistro_ManifestError(t *testing.T) {
 			imgOpts := distro.ImageOptions{
 				Size: imgType.Size(0),
 			}
-			_, err := imgType.Manifest(bp.Customizations, imgOpts, nil, nil, 0)
+			testPackageSpecSets := distro_test_common.GetTestingPackageSpecSets("kernel", arch.Name(), imgType.PayloadPackageSets())
+			_, err := imgType.Manifest(bp.Customizations, imgOpts, nil, testPackageSpecSets, 0)
 			if imgTypeName == "edge-commit" || imgTypeName == "edge-container" {
 				assert.EqualError(t, err, "kernel boot parameter customizations are not supported for ostree types")
 			} else if imgTypeName == "edge-raw-image" {
@@ -618,7 +619,8 @@ func TestDistro_TestRootMountPoint(t *testing.T) {
 		arch, _ := r8distro.GetArch(archName)
 		for _, imgTypeName := range arch.ListImageTypes() {
 			imgType, _ := arch.GetImageType(imgTypeName)
-			_, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, nil, 0)
+			testPackageSpecSets := distro_test_common.GetTestingPackageSpecSets("kernel", arch.Name(), imgType.PayloadPackageSets())
+			_, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, testPackageSpecSets, 0)
 			if imgTypeName == "edge-commit" || imgTypeName == "edge-container" {
 				assert.EqualError(t, err, "Custom mountpoints are not supported for ostree types")
 			} else if imgTypeName == "edge-installer" || imgTypeName == "edge-simplified-installer" || imgTypeName == "edge-raw-image" {
@@ -650,7 +652,8 @@ func TestDistro_CustomFileSystemSubDirectories(t *testing.T) {
 		arch, _ := r8distro.GetArch(archName)
 		for _, imgTypeName := range arch.ListImageTypes() {
 			imgType, _ := arch.GetImageType(imgTypeName)
-			_, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, nil, 0)
+			testPackageSpecSets := distro_test_common.GetTestingPackageSpecSets("kernel", arch.Name(), imgType.PayloadPackageSets())
+			_, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, testPackageSpecSets, 0)
 			if strings.HasPrefix(imgTypeName, "edge-") {
 				continue
 			} else {
@@ -688,7 +691,8 @@ func TestDistro_MountpointsWithArbitraryDepthAllowed(t *testing.T) {
 		arch, _ := r8distro.GetArch(archName)
 		for _, imgTypeName := range arch.ListImageTypes() {
 			imgType, _ := arch.GetImageType(imgTypeName)
-			_, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, nil, 0)
+			testPackageSpecSets := distro_test_common.GetTestingPackageSpecSets("kernel", arch.Name(), imgType.PayloadPackageSets())
+			_, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, testPackageSpecSets, 0)
 			if strings.HasPrefix(imgTypeName, "edge-") {
 				continue
 			} else {
@@ -780,7 +784,8 @@ func TestDistro_CustomUsrPartitionNotLargeEnough(t *testing.T) {
 		arch, _ := r8distro.GetArch(archName)
 		for _, imgTypeName := range arch.ListImageTypes() {
 			imgType, _ := arch.GetImageType(imgTypeName)
-			_, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, nil, 0)
+			testPackageSpecSets := distro_test_common.GetTestingPackageSpecSets("kernel", arch.Name(), imgType.PayloadPackageSets())
+			_, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, testPackageSpecSets, 0)
 			if imgTypeName == "edge-commit" || imgTypeName == "edge-container" {
 				assert.EqualError(t, err, "Custom mountpoints are not supported for ostree types")
 			} else if imgTypeName == "edge-installer" || imgTypeName == "edge-simplified-installer" || imgTypeName == "edge-raw-image" {
diff --git a/internal/distro/rhel90/distro_test.go b/internal/distro/rhel90/distro_test.go
index c91c707ea..e13b9d06c 100644
--- a/internal/distro/rhel90/distro_test.go
+++ b/internal/distro/rhel90/distro_test.go
@@ -424,7 +424,8 @@ func TestDistro_ManifestError(t *testing.T) {
 			imgOpts := distro.ImageOptions{
 				Size: imgType.Size(0),
 			}
-			_, err := imgType.Manifest(bp.Customizations, imgOpts, nil, nil, 0)
+			testPackageSpecSets := distro_test_common.GetTestingPackageSpecSets("kernel", arch.Name(), imgType.PayloadPackageSets())
+			_, err := imgType.Manifest(bp.Customizations, imgOpts, nil, testPackageSpecSets, 0)
 			if imgTypeName == "edge-commit" || imgTypeName == "edge-container" {
 				assert.EqualError(t, err, "kernel boot parameter customizations are not supported for ostree types")
 			} else if imgTypeName == "edge-raw-image" {
@@ -618,7 +619,8 @@ func TestDistro_TestRootMountPoint(t *testing.T) {
 		arch, _ := r9distro.GetArch(archName)
 		for _, imgTypeName := range arch.ListImageTypes() {
 			imgType, _ := arch.GetImageType(imgTypeName)
-			_, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, nil, 0)
+			testPackageSpecSets := distro_test_common.GetTestingPackageSpecSets("kernel", arch.Name(), imgType.PayloadPackageSets())
+			_, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, testPackageSpecSets, 0)
 			if imgTypeName == "edge-commit" || imgTypeName == "edge-container" {
 				assert.EqualError(t, err, "Custom mountpoints are not supported for ostree types")
 			} else if imgTypeName == "edge-installer" || imgTypeName == "edge-simplified-installer" || imgTypeName == "edge-raw-image" {
@@ -650,7 +652,8 @@ func TestDistro_CustomFileSystemSubDirectories(t *testing.T) {
 		arch, _ := r9distro.GetArch(archName)
 		for _, imgTypeName := range arch.ListImageTypes() {
 			imgType, _ := arch.GetImageType(imgTypeName)
-			_, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, nil, 0)
+			testPackageSpecSets := distro_test_common.GetTestingPackageSpecSets("kernel", arch.Name(), imgType.PayloadPackageSets())
+			_, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, testPackageSpecSets, 0)
 			if strings.HasPrefix(imgTypeName, "edge-") {
 				continue
 			} else {
@@ -688,7 +691,8 @@ func TestDistro_MountpointsWithArbitraryDepthAllowed(t *testing.T) {
 		arch, _ := r9distro.GetArch(archName)
 		for _, imgTypeName := range arch.ListImageTypes() {
 			imgType, _ := arch.GetImageType(imgTypeName)
-			_, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, nil, 0)
+			testPackageSpecSets := distro_test_common.GetTestingPackageSpecSets("kernel", arch.Name(), imgType.PayloadPackageSets())
+			_, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, testPackageSpecSets, 0)
 			if strings.HasPrefix(imgTypeName, "edge-") {
 				continue
 			} else {
@@ -780,7 +784,8 @@ func TestDistro_CustomUsrPartitionNotLargeEnough(t *testing.T) {
 		arch, _ := r9distro.GetArch(archName)
 		for _, imgTypeName := range arch.ListImageTypes() {
 			imgType, _ := arch.GetImageType(imgTypeName)
-			_, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, nil, 0)
+			testPackageSpecSets := distro_test_common.GetTestingPackageSpecSets("kernel", arch.Name(), imgType.PayloadPackageSets())
+			_, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, testPackageSpecSets, 0)
 			if imgTypeName == "edge-commit" || imgTypeName == "edge-container" {
 				assert.EqualError(t, err, "Custom mountpoints are not supported for ostree types")
 			} else if imgTypeName == "edge-installer" || imgTypeName == "edge-simplified-installer" || imgTypeName == "edge-raw-image" {
diff --git a/internal/distro/rhel90beta/distro_test.go b/internal/distro/rhel90beta/distro_test.go
index 0350c9445..ac40a7fda 100644
--- a/internal/distro/rhel90beta/distro_test.go
+++ b/internal/distro/rhel90beta/distro_test.go
@@ -424,7 +424,8 @@ func TestDistro_ManifestError(t *testing.T) {
 			imgOpts := distro.ImageOptions{
 				Size: imgType.Size(0),
 			}
-			_, err := imgType.Manifest(bp.Customizations, imgOpts, nil, nil, 0)
+			testPackageSpecSets := distro_test_common.GetTestingPackageSpecSets("kernel", arch.Name(), imgType.PayloadPackageSets())
+			_, err := imgType.Manifest(bp.Customizations, imgOpts, nil, testPackageSpecSets, 0)
 			if imgTypeName == "edge-commit" || imgTypeName == "edge-container" {
 				assert.EqualError(t, err, "kernel boot parameter customizations are not supported for ostree types")
 			} else if imgTypeName == "edge-installer" {
@@ -611,7 +612,8 @@ func TestDistro_TestRootMountPoint(t *testing.T) {
 		arch, _ := r9distro.GetArch(archName)
 		for _, imgTypeName := range arch.ListImageTypes() {
 			imgType, _ := arch.GetImageType(imgTypeName)
-			_, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, nil, 0)
+			testPackageSpecSets := distro_test_common.GetTestingPackageSpecSets("kernel", arch.Name(), imgType.PayloadPackageSets())
+			_, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, testPackageSpecSets, 0)
 			if imgTypeName == "edge-commit" || imgTypeName == "edge-container" {
 				assert.EqualError(t, err, "Custom mountpoints are not supported for ostree types")
 			} else if imgTypeName == "edge-installer" {
@@ -643,7 +645,8 @@ func TestDistro_CustomFileSystemSubDirectories(t *testing.T) {
 		arch, _ := r9distro.GetArch(archName)
 		for _, imgTypeName := range arch.ListImageTypes() {
 			imgType, _ := arch.GetImageType(imgTypeName)
-			_, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, nil, 0)
+			testPackageSpecSets := distro_test_common.GetTestingPackageSpecSets("kernel", arch.Name(), imgType.PayloadPackageSets())
+			_, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, testPackageSpecSets, 0)
 			if strings.HasPrefix(imgTypeName, "edge-") {
 				continue
 			} else {
@@ -681,7 +684,8 @@ func TestDistro_MountpointsWithArbitraryDepthAllowed(t *testing.T) {
 		arch, _ := r9distro.GetArch(archName)
 		for _, imgTypeName := range arch.ListImageTypes() {
 			imgType, _ := arch.GetImageType(imgTypeName)
-			_, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, nil, 0)
+			testPackageSpecSets := distro_test_common.GetTestingPackageSpecSets("kernel", arch.Name(), imgType.PayloadPackageSets())
+			_, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, testPackageSpecSets, 0)
 			if strings.HasPrefix(imgTypeName, "edge-") {
 				continue
 			} else {
@@ -773,7 +777,8 @@ func TestDistro_CustomUsrPartitionNotLargeEnough(t *testing.T) {
 		arch, _ := r9distro.GetArch(archName)
 		for _, imgTypeName := range arch.ListImageTypes() {
 			imgType, _ := arch.GetImageType(imgTypeName)
-			_, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, nil, 0)
+			testPackageSpecSets := distro_test_common.GetTestingPackageSpecSets("kernel", arch.Name(), imgType.PayloadPackageSets())
+			_, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, testPackageSpecSets, 0)
 			if imgTypeName == "edge-commit" || imgTypeName == "edge-container" {
 				assert.EqualError(t, err, "Custom mountpoints are not supported for ostree types")
 			} else if imgTypeName == "edge-installer" {
diff --git a/internal/osbuild2/rpm_stage_test.go b/internal/osbuild2/rpm_stage_test.go
index fbc73f1e2..a23e55eb2 100644
--- a/internal/osbuild2/rpm_stage_test.go
+++ b/internal/osbuild2/rpm_stage_test.go
@@ -1,9 +1,13 @@
 package osbuild2
 
 import (
+	"encoding/json"
+	"fmt"
 	"testing"
 
+	"github.com/osbuild/osbuild-composer/internal/rpmmd"
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 )
 
 func TestNewRPMStage(t *testing.T) {
@@ -15,3 +19,71 @@ func TestNewRPMStage(t *testing.T) {
 	actualStage := NewRPMStage(&RPMStageOptions{}, &RPMStageInputs{})
 	assert.Equal(t, expectedStage, actualStage)
 }
+
+func Test_OSBuildMetadataToRPMs(t *testing.T) {
+	raw := `
+{
+  "org.osbuild.rpm": {
+    "packages": [
+      {
+        "name": "python3-pyserial",
+        "version": "3.4",
+        "release": "7.fc32",
+        "epoch": null,
+        "arch": "noarch",
+        "sigmd5": "378cb32f9f850b275ac4e04d21e8144b",
+        "sigpgp": "89023304000108001d162104963a2beb02009608fe67ea4249fd77499570ff3105025f5a272b000a091049fd77499570ff31ccdb0ffe38b95a55ebf3c021526b3cd4f2358c7e23f7767d1f5ce4b7cccef7b33653c6a96a23022313a818fbaf7abeb41837910f0d3ac15664e02838d5939d38ff459aa0076e248728a032d3ae09ddfaec955f941601081a2e3f9bbd49586fd65c1bc1b31685aeb0405687d1791471eab7359ccf00d5584ddef680e99ebc8a4846316391b9baa68ac8ed8ad696ee16fd625d847f8edd92517df3ea6920a46b77b4f119715a0f619f38835d25e0bd0eb5cfad08cd9c796eace6a2b28f4d3dee552e6068255d9748dc2a1906c951e0ba8aed9922ab24e1f659413a06083f8a0bfea56cfff14bddef23bced449f36bcd369da72f90ddf0512e7b0801ba5a0c8eaa8eb0582c630815e992192042cfb0a7c7239f76219197c2fdf18b6553260c105280806d4f037d7b04bdf3da9fd7e9a207db5c71f7e548f4288928f047c989c4cb9cbb8088eec7bd2fa5c252e693f51a3cfc660f666af6a255a5ca0fd2216d5ccd66cbd9c11afa61067d7f615ec8d0dc0c879b5fe633d8c9443f97285da597e4da8a3993af36f0be06acfa9b8058ec70bbc78b876e4c6c5d2108fb05c15a74ba48a3d7ded697cbc1748c228d77d1e0794a41fd5240fa67c3ed745fe47555a47c3d6163d8ce95fd6c2d0d6fa48f8e5b411e571e442109b1cb200d9a8117ee08bfe645f96aca34f7b7559622bbab75143dcad59f126ae0d319e6668ebba417e725638c4febf2e",
+        "siggpg": "883f0305005f2310139ec3e4c0f7e257e611023e11009f639c5fe64abaa76224dab3a9f70c2714a84c63bd009d1cc184fb4b428dfcd7c3556f4a5f860cc0187740"
+      },
+      {
+        "name": "libgcc",
+        "version": "10.0.1",
+        "release": "0.11.fc32",
+        "epoch": null,
+        "arch": "x86_64",
+        "sigmd5": "84fc907a5047aeebaf8da1642925a417",
+        "sigpgp": "89023304000108001d162104963a2beb02009608fe67ea4249fd77499570ff3105025f5a272b000a091049fd77499570ff31ccdb0ffe38b95a55ebf3c021526b3cd4f2358c7e23f7767d1f5ce4b7cccef7b33653c6a96a23022313a818fbaf7abeb41837910f0d3ac15664e02838d5939d38ff459aa0076e248728a032d3ae09ddfaec955f941601081a2e3f9bbd49586fd65c1bc1b31685aeb0405687d1791471eab7359ccf00d5584ddef680e99ebc8a4846316391b9baa68ac8ed8ad696ee16fd625d847f8edd92517df3ea6920a46b77b4f119715a0f619f38835d25e0bd0eb5cfad08cd9c796eace6a2b28f4d3dee552e6068255d9748dc2a1906c951e0ba8aed9922ab24e1f659413a06083f8a0bfea56cfff14bddef23bced449f36bcd369da72f90ddf0512e7b0801ba5a0c8eaa8eb0582c630815e992192042cfb0a7c7239f76219197c2fdf18b6553260c105280806d4f037d7b04bdf3da9fd7e9a207db5c71f7e548f4288928f047c989c4cb9cbb8088eec7bd2fa5c252e693f51a3cfc660f666af6a255a5ca0fd2216d5ccd66cbd9c11afa61067d7f615ec8d0dc0c879b5fe633d8c9443f97285da597e4da8a3993af36f0be06acfa9b8058ec70bbc78b876e4c6c5d2108fb05c15a74ba48a3d7ded697cbc1748c228d77d1e0794a41fd5240fa67c3ed745fe47555a47c3d6163d8ce95fd6c2d0d6fa48f8e5b411e571e442109b1cb200d9a8117ee08bfe645f96aca34f7b7559622bbab75143dcad59f126ae0d319e6668ebba417e725638c4febf2e",
+        "siggpg": null
+      },
+      {
+        "name": "libgcc-madeup",
+        "version": "10.0.1",
+        "release": "0.11.fc32",
+        "epoch": null,
+        "arch": "x86_64",
+        "sigmd5": "84fc907a5047aeebaf8da1642925a418",
+        "sigpgp": null,
+        "siggpg": null
+      }
+    ]
+  }
+}
+`
+	metadata := new(PipelineMetadata)
+	err := json.Unmarshal([]byte(raw), metadata)
+	require.NoError(t, err)
+
+	fmt.Printf("Result: %#v", metadata)
+	rpms := OSBuildMetadataToRPMs(*metadata)
+
+	require.Len(t, rpms, 3)
+
+	signature1 := "89023304000108001d162104963a2beb02009608fe67ea4249fd77499570ff3105025f5a272b000a091049fd77499570ff31ccdb0ffe38b95a55ebf3c021526b3cd4f2358c7e23f7767d1f5ce4b7cccef7b33653c6a96a23022313a818fbaf7abeb41837910f0d3ac15664e02838d5939d38ff459aa0076e248728a032d3ae09ddfaec955f941601081a2e3f9bbd49586fd65c1bc1b31685aeb0405687d1791471eab7359ccf00d5584ddef680e99ebc8a4846316391b9baa68ac8ed8ad696ee16fd625d847f8edd92517df3ea6920a46b77b4f119715a0f619f38835d25e0bd0eb5cfad08cd9c796eace6a2b28f4d3dee552e6068255d9748dc2a1906c951e0ba8aed9922ab24e1f659413a06083f8a0bfea56cfff14bddef23bced449f36bcd369da72f90ddf0512e7b0801ba5a0c8eaa8eb0582c630815e992192042cfb0a7c7239f76219197c2fdf18b6553260c105280806d4f037d7b04bdf3da9fd7e9a207db5c71f7e548f4288928f047c989c4cb9cbb8088eec7bd2fa5c252e693f51a3cfc660f666af6a255a5ca0fd2216d5ccd66cbd9c11afa61067d7f615ec8d0dc0c879b5fe633d8c9443f97285da597e4da8a3993af36f0be06acfa9b8058ec70bbc78b876e4c6c5d2108fb05c15a74ba48a3d7ded697cbc1748c228d77d1e0794a41fd5240fa67c3ed745fe47555a47c3d6163d8ce95fd6c2d0d6fa48f8e5b411e571e442109b1cb200d9a8117ee08bfe645f96aca34f7b7559622bbab75143dcad59f126ae0d319e6668ebba417e725638c4febf2e"
+	require.Equal(t, rpmmd.RPM{
+		Type:      "rpm",
+		Name:      "libgcc",
+		Version:   "10.0.1",
+		Release:   "0.11.fc32",
+		Epoch:     nil,
+		Arch:      "x86_64",
+		Sigmd5:    "84fc907a5047aeebaf8da1642925a417",
+		Signature: &signature1,
+	}, rpms[1])
+
+	// GPG has a priority over PGP
+	signature0 := "883f0305005f2310139ec3e4c0f7e257e611023e11009f639c5fe64abaa76224dab3a9f70c2714a84c63bd009d1cc184fb4b428dfcd7c3556f4a5f860cc0187740"
+	require.Equal(t, signature0, *rpms[0].Signature)
+
+	// if neither GPG nor PGP is set, the signature is nil
+	require.Nil(t, rpms[2].Signature)
+}
diff --git a/internal/rpmmd/metadata_test.go b/internal/rpmmd/metadata_test.go
index b504825c6..7f5786305 100644
--- a/internal/rpmmd/metadata_test.go
+++ b/internal/rpmmd/metadata_test.go
@@ -1,85 +1,16 @@
-package rpmmd
+// a different package is used to prevent import cycles between `rpmmd` and `osbuild2`
+package rpmmd_test
 
 import (
-	"encoding/json"
-	"fmt"
 	"sort"
 	"testing"
 
 	"github.com/osbuild/osbuild-composer/internal/common"
 	osbuild "github.com/osbuild/osbuild-composer/internal/osbuild2"
+	"github.com/osbuild/osbuild-composer/internal/rpmmd"
 	"github.com/stretchr/testify/assert"
-	"github.com/stretchr/testify/require"
 )
 
-func Test_osbuildStagesToRPMs(t *testing.T) {
-	raw := `
-{
-  "org.osbuild.rpm": {
-    "packages": [
-      {
-        "name": "python3-pyserial",
-        "version": "3.4",
-        "release": "7.fc32",
-        "epoch": null,
-        "arch": "noarch",
-        "sigmd5": "378cb32f9f850b275ac4e04d21e8144b",
-        "sigpgp": "89023304000108001d162104963a2beb02009608fe67ea4249fd77499570ff3105025f5a272b000a091049fd77499570ff31ccdb0ffe38b95a55ebf3c021526b3cd4f2358c7e23f7767d1f5ce4b7cccef7b33653c6a96a23022313a818fbaf7abeb41837910f0d3ac15664e02838d5939d38ff459aa0076e248728a032d3ae09ddfaec955f941601081a2e3f9bbd49586fd65c1bc1b31685aeb0405687d1791471eab7359ccf00d5584ddef680e99ebc8a4846316391b9baa68ac8ed8ad696ee16fd625d847f8edd92517df3ea6920a46b77b4f119715a0f619f38835d25e0bd0eb5cfad08cd9c796eace6a2b28f4d3dee552e6068255d9748dc2a1906c951e0ba8aed9922ab24e1f659413a06083f8a0bfea56cfff14bddef23bced449f36bcd369da72f90ddf0512e7b0801ba5a0c8eaa8eb0582c630815e992192042cfb0a7c7239f76219197c2fdf18b6553260c105280806d4f037d7b04bdf3da9fd7e9a207db5c71f7e548f4288928f047c989c4cb9cbb8088eec7bd2fa5c252e693f51a3cfc660f666af6a255a5ca0fd2216d5ccd66cbd9c11afa61067d7f615ec8d0dc0c879b5fe633d8c9443f97285da597e4da8a3993af36f0be06acfa9b8058ec70bbc78b876e4c6c5d2108fb05c15a74ba48a3d7ded697cbc1748c228d77d1e0794a41fd5240fa67c3ed745fe47555a47c3d6163d8ce95fd6c2d0d6fa48f8e5b411e571e442109b1cb200d9a8117ee08bfe645f96aca34f7b7559622bbab75143dcad59f126ae0d319e6668ebba417e725638c4febf2e",
-        "siggpg": "883f0305005f2310139ec3e4c0f7e257e611023e11009f639c5fe64abaa76224dab3a9f70c2714a84c63bd009d1cc184fb4b428dfcd7c3556f4a5f860cc0187740"
-      },
-      {
-        "name": "libgcc",
-        "version": "10.0.1",
-        "release": "0.11.fc32",
-        "epoch": null,
-        "arch": "x86_64",
-        "sigmd5": "84fc907a5047aeebaf8da1642925a417",
-        "sigpgp": "89023304000108001d162104963a2beb02009608fe67ea4249fd77499570ff3105025f5a272b000a091049fd77499570ff31ccdb0ffe38b95a55ebf3c021526b3cd4f2358c7e23f7767d1f5ce4b7cccef7b33653c6a96a23022313a818fbaf7abeb41837910f0d3ac15664e02838d5939d38ff459aa0076e248728a032d3ae09ddfaec955f941601081a2e3f9bbd49586fd65c1bc1b31685aeb0405687d1791471eab7359ccf00d5584ddef680e99ebc8a4846316391b9baa68ac8ed8ad696ee16fd625d847f8edd92517df3ea6920a46b77b4f119715a0f619f38835d25e0bd0eb5cfad08cd9c796eace6a2b28f4d3dee552e6068255d9748dc2a1906c951e0ba8aed9922ab24e1f659413a06083f8a0bfea56cfff14bddef23bced449f36bcd369da72f90ddf0512e7b0801ba5a0c8eaa8eb0582c630815e992192042cfb0a7c7239f76219197c2fdf18b6553260c105280806d4f037d7b04bdf3da9fd7e9a207db5c71f7e548f4288928f047c989c4cb9cbb8088eec7bd2fa5c252e693f51a3cfc660f666af6a255a5ca0fd2216d5ccd66cbd9c11afa61067d7f615ec8d0dc0c879b5fe633d8c9443f97285da597e4da8a3993af36f0be06acfa9b8058ec70bbc78b876e4c6c5d2108fb05c15a74ba48a3d7ded697cbc1748c228d77d1e0794a41fd5240fa67c3ed745fe47555a47c3d6163d8ce95fd6c2d0d6fa48f8e5b411e571e442109b1cb200d9a8117ee08bfe645f96aca34f7b7559622bbab75143dcad59f126ae0d319e6668ebba417e725638c4febf2e",
-        "siggpg": null
-      },
-      {
-        "name": "libgcc-madeup",
-        "version": "10.0.1",
-        "release": "0.11.fc32",
-        "epoch": null,
-        "arch": "x86_64",
-        "sigmd5": "84fc907a5047aeebaf8da1642925a418",
-        "sigpgp": null,
-        "siggpg": null
-      }
-    ]
-  }
-}
-`
-	metadata := new(osbuild.PipelineMetadata)
-	err := json.Unmarshal([]byte(raw), metadata)
-	require.NoError(t, err)
-
-	fmt.Printf("Result: %#v", metadata)
-	rpms := OSBuildMetadataToRPMs(*metadata)
-
-	require.Len(t, rpms, 3)
-
-	signature1 := "89023304000108001d162104963a2beb02009608fe67ea4249fd77499570ff3105025f5a272b000a091049fd77499570ff31ccdb0ffe38b95a55ebf3c021526b3cd4f2358c7e23f7767d1f5ce4b7cccef7b33653c6a96a23022313a818fbaf7abeb41837910f0d3ac15664e02838d5939d38ff459aa0076e248728a032d3ae09ddfaec955f941601081a2e3f9bbd49586fd65c1bc1b31685aeb0405687d1791471eab7359ccf00d5584ddef680e99ebc8a4846316391b9baa68ac8ed8ad696ee16fd625d847f8edd92517df3ea6920a46b77b4f119715a0f619f38835d25e0bd0eb5cfad08cd9c796eace6a2b28f4d3dee552e6068255d9748dc2a1906c951e0ba8aed9922ab24e1f659413a06083f8a0bfea56cfff14bddef23bced449f36bcd369da72f90ddf0512e7b0801ba5a0c8eaa8eb0582c630815e992192042cfb0a7c7239f76219197c2fdf18b6553260c105280806d4f037d7b04bdf3da9fd7e9a207db5c71f7e548f4288928f047c989c4cb9cbb8088eec7bd2fa5c252e693f51a3cfc660f666af6a255a5ca0fd2216d5ccd66cbd9c11afa61067d7f615ec8d0dc0c879b5fe633d8c9443f97285da597e4da8a3993af36f0be06acfa9b8058ec70bbc78b876e4c6c5d2108fb05c15a74ba48a3d7ded697cbc1748c228d77d1e0794a41fd5240fa67c3ed745fe47555a47c3d6163d8ce95fd6c2d0d6fa48f8e5b411e571e442109b1cb200d9a8117ee08bfe645f96aca34f7b7559622bbab75143dcad59f126ae0d319e6668ebba417e725638c4febf2e"
-	require.Equal(t, RPM{
-		Type:      "rpm",
-		Name:      "libgcc",
-		Version:   "10.0.1",
-		Release:   "0.11.fc32",
-		Epoch:     nil,
-		Arch:      "x86_64",
-		Sigmd5:    "84fc907a5047aeebaf8da1642925a417",
-		Signature: &signature1,
-	}, rpms[1])
-
-	// GPG has a priority over PGP
-	signature0 := "883f0305005f2310139ec3e4c0f7e257e611023e11009f639c5fe64abaa76224dab3a9f70c2714a84c63bd009d1cc184fb4b428dfcd7c3556f4a5f860cc0187740"
-	require.Equal(t, signature0, *rpms[0].Signature)
-
-	// if neither GPG nor PGP is set, the signature is nil
-	require.Nil(t, rpms[2].Signature)
-}
-
 func TestRPMDeduplication(t *testing.T) {
 	assert := assert.New(t)
 	// start with metadata, that includes duplicates, convert, then deduplicate
@@ -183,12 +114,12 @@ func TestRPMDeduplication(t *testing.T) {
 	testNames := []string{"dupename", "dupename", "package-with-epoch", "python38", "python38", "unique", "vim-minimal", "vim-minimal"}
 	testNamesDeduped := []string{"dupename", "dupename", "package-with-epoch", "python38", "unique", "vim-minimal"}
 
-	rpms := OSBuildMetadataToRPMs(metadata)
+	rpms := osbuild.OSBuildMetadataToRPMs(metadata)
 
 	// basic sanity checks
 	assert.Len(rpms, 8)
 
-	sortedNames := func(rpms []RPM) []string {
+	sortedNames := func(rpms []rpmmd.RPM) []string {
 		names := make([]string, len(rpms))
 		for idx, rpm := range rpms {
 			names[idx] = rpm.Name
@@ -201,7 +132,7 @@ func TestRPMDeduplication(t *testing.T) {
 	names := sortedNames(rpms)
 	assert.Equal(names, testNames)
 
-	deduped := DeduplicateRPMs(rpms)
+	deduped := rpmmd.DeduplicateRPMs(rpms)
 	assert.Len(deduped, 6)
 	dedupedNames := sortedNames(deduped)
 	assert.Equal(dedupedNames, testNamesDeduped)
diff --git a/test/cases/upgrade8to9.sh b/test/cases/upgrade8to9.sh
index b8e6a2231..485c67d88 100755
--- a/test/cases/upgrade8to9.sh
+++ b/test/cases/upgrade8to9.sh
@@ -121,6 +121,8 @@ sudo pkill -P "$CONSOLE_PID"
 sudo scp "${SSH_OPTIONS[@]}" -q -i "${SSH_KEY}" /usr/share/tests/osbuild-composer/upgrade8to9/*.sh root@"$INSTANCE_ADDRESS":
 set +e
 sudo ssh "${SSH_OPTIONS[@]}" -q -i "${SSH_KEY}" root@"$INSTANCE_ADDRESS" 'source /root/upgrade_prepare.sh'
+sudo scp "${SSH_OPTIONS[@]}" -q -i "${SSH_KEY}" root@"$INSTANCE_ADDRESS":/var/log/leapp/leapp-preupgrade.log .
+sudo scp "${SSH_OPTIONS[@]}" -q -i "${SSH_KEY}" root@"$INSTANCE_ADDRESS":/var/log/leapp/leapp-report.txt .
 set -e
 
 # watch and log the console during upgrade
diff --git a/test/data/upgrade8to9/upgrade_prepare.sh b/test/data/upgrade8to9/upgrade_prepare.sh
index b3673fc23..74ed81011 100755
--- a/test/data/upgrade8to9/upgrade_prepare.sh
+++ b/test/data/upgrade8to9/upgrade_prepare.sh
@@ -32,6 +32,9 @@ enabled=0
 gpgcheck=0
 EOF
 
+# AllowZoneDrifting is disabled in RHEL-9, see rhbz#2054271 for more details
+sed -i "s/^AllowZoneDrifting=.*/AllowZoneDrifting=no/" /etc/firewalld/firewalld.conf
+
 # check upgrade
 leapp preupgrade --debug --no-rhsm