Repository: osbuild/osbuild-composer
Pull request #3659 (2023-08-25 15:15:46)

Consolidation & extension of information imported to Koji builds
This PR is related to [COMPOSER-1801](https://issues.redhat.com/browse/COMPOSER-1801).

The main intention of this PR is to enhance osbuild-composer as a Koji Content Generator, to import all of the most important artifacts to the Koji build, with additional metadata. The goal is to ensure that these additional files and data are preserved for the lifetime of the image in a Koji build.

Previously, some files such as OSBuild logs and manifests were uploaded only to the Koji task by the `koji-osbuild` builder plugin. This resulted in an undesirable situation of them being garbage collected after some time, thus making it extremely difficult (close to impossible) to reproduce the same image build using OSBuild if needed.

With this PR, all the information needed to reproduce an image build are attached to the Koji build and preserved with the image file itself.

## Main changes from Koji PoV

1. OSBuild manifests for the images attached to the Koji build are now also attached to the Koji build as an output archive (in the Koji nomenclature).
2. OSBuild logs from image builds are now attached as logs to the Koji build. These are not JSON files, but a human readable text files with the same format as produced by Image Builder on-prem (when using Weldr API).
3. OSBuild manifest and image archives have additional extra metadata attached to them, and the same metadata are also combined and attached to the build itself.

### Extra metadata attached to OSBuild manifests

The metadata attached to OSBuild manifests are intended to make it easy to reproduce the creation of the manifest used for image build and to expose versions of all relevant tools and dependencies which can affect the resulting manifest.

Attached information:

- **Architecture** of the manifest. Manifest itself is a JSON, but the resulting image is architecture-specific.
- **osbuild-composer version** which generated the manifest. The version is determined by https://github.com/osbuild/osbuild-composer/blob/9d7159dab36efce8035f35b3cdaa0e9d9438f799/internal/common/constants.go#L30-L38
- List of selected **osbuild-composer Go dependencies versions**, which could affect the manifest content. For now, only the `osbuild/images` dependency is included, since it contains the actual image definitions.

Example extra metadata attached to a manifest archive:

```json
{
  "image": {
    "arch": "x86_64",
    "info": {
      "osbuild_composer_version": "git-rev:08a6029ea0b5add394c7ce464e63dffb899790d9",
      "osbuild_composer_deps": [
        {
          "path": "github.com/osbuild/images",
          "version": "v0.6.1-0.20230922185004-1318eb3fc554"
        }
      ]
    }
  }
}
```

### Extra metadata attached to the image

The metadata attached the image are intended to:

- Make it easy to reproduce the image build.
- Make it easy to locate the image in the target cloud environment if it was specified when the build was triggered.
- Provide additional context and information about the image (such as the boot mode).

Attached information:

- **Architecture** of the image.
- **Boot mode** of the image.
- Information about the **osbuild artifact**. Specifically, the **name of the manifest pipeline**, which was exported to produce the image and the **filename of the exported file**.
- **OSBuild version** which was used by the worker to produce the image. This is the version determined by `osbuild --version`.
- **Upload target results** , for any non-Koji upload target. It is a list of target-specific information to make it possible to locate the uploaded image in the target environment. In addition, also the target-specific **osbuild artifact** information is included. Technically, a different pipeline could have been exported from the same OSBuild manifest for each upload target. In reality, this is never happening at the moment. 

Example extra metadata attached to a GCP image uploaded to GCP:

```json
{
  "image": {
    "arch": "x86_64",
    "boot_mode": "uefi",
    "osbuild_artifact": {
      "export_filename": "image.tar.gz",
      "export_name": "archive"
    },
    "osbuild_version": "93",
    "upload_target_results": [
      {
        "name": "org.osbuild.gcp",
        "options": {
          "image_name": "composer-api-82b40e29-95e9-4b0c-8935-dd401086b65a",
          "project_id": "cockpituous"
        },
        "osbuild_artifact": {
          "export_filename": "image.tar.gz",
          "export_name": "archive"
        }
      }
    ]
  }
}
```

### Extra metadata attached to the build

The extra metadata attached to the build just combines the extra metadata attached to each specific output archive attached to the Koji build.

Example extra metadata attached to a build:

```json
{
  "typeinfo": {
    "image": {
      "name-version-release.x86_64.tar.gz": {
        "arch": "x86_64",
        "boot_mode": "uefi",
        "osbuild_artifact": {
          "export_filename": "image.tar.gz",
          "export_name": "archive"
        },
        "osbuild_version": "93",
        "upload_target_results": [
          {
            "name": "org.osbuild.gcp",
            "options": {
              "image_name": "composer-api-82b40e29-95e9-4b0c-8935-dd401086b65a",
              "project_id": "cockpituous"
            },
            "osbuild_artifact": {
              "export_filename": "image.tar.gz",
              "export_name": "archive"
            }
          }
        ]
      }
    }
  },
  "osbuild_manifest": {
    "name-version-release.x86_64.tar.gz.manifest.json": {
      "arch": "x86_64",
      "info": {
        "osbuild_composer_version": "git-rev:08a6029ea0b5add394c7ce464e63dffb899790d9",
        "osbuild_composer_deps": [
          {
            "path": "github.com/osbuild/images",
            "version": "v0.6.1-0.20230922185004-1318eb3fc554"
          }
        ]
      }
    }
  }
}
```

## Main changes from osbuild-composer PoV

Besides the changes mentioned above, which imply direct changes to Koji-related code in osbuild-composer, the following notable changes happened in the rest of the osbuild-composer code:

- `target.OsbuildArtifact` is now included also in the `target.TargetResult` and copied from the `target.Target` by the worker. This makes it possible to access the information by any job which depends on the `OSBuildJob`. Specifically, the `koji-finalize` job is taking advantage of it.
- `worker.ManifestJobByIDResult` now includes the information about the `osbuild-composer` version which produced the manifest.
- `worker.ManifestJobByIDResult` now includes the information about the `osbuild/images` version used by `osbuild-composer` which produced the manifest. If osbuild-composer is not compiled with the support for Go modules, this information can't be retrieved and it is not included in the result structure.
- `worker.OSBuildJobResult` now includes the `osbuild` version used by the worker to produce the image.

This pull request includes:

- [x] adequate testing for the new functionality or fixed issue
  - `koji.sh` have been extended to cover the newly added functionality.
  - Some new unit tests have been added.
- [ ] adequate documentation informing people about the change such as
  - **TBD** 
  - [ ] 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`

Information regarding our GitLab pipeline (Schutzbot):

CI will not be ran automatically if WIP label is applied or the PR is in DRAFT state, instead only
a link will be provided to the pipeline which can then be triggered manually if desired. To run the
CI automatically either switch the PR to ready or apply WIP+test label.

Outside contributors need manual approval from one of the osbuild-composer maintainers.

Schutzbot will only be triggered if all Tests jobs in GitHub workflow succeed.
-->


thozza: The code is good enough (passed Koji tests as is) to open it for review. I still want to iterate over all commits and fix any typos or mistakes, but want to receive any feedback, since I expect some discussion...
thozza: It turns out that we build Fedora RPM in a different way, than CS / RHEL RPMs, which makes it impossible to get the version of `osbuild/images` dependency. I'm digging more into it...
thozza: > > It turns out that we build Fedora RPM in a different way, than CS / RHEL RPMs, which makes it impossible to get the version of `osbuild/images` dependency. I'm digging more into it...
> 
> I haven't looked at the code in detail yet but I read through the description and comments. Is it necessary to know the version of the dependency? Outside of development and testing, an osbuild-composer version or commit ID is tied to the dependency version. It requires an extra step to get to it (you need to lookup the composer version and find the dependency), but it's probably not worth the effort to dig out the version, is it? Am I missing something?

The composer version can be just a package NEVRA, instead of a git-rev, which would make it a bit more complicated, although not impossible, to determine the `images` version. In reality, our service always runs RPMs built from git checkout, so it should not happen with our deployment. I just thought that it would be very convenient to see the exact `images` version to be able to check the used image definition without any extra steps.

The effort to dig out the version is not really big, we just build the RPM on Fedora probably in a weird way since we moved to using vendored modules. There are standard library functions to get the list of deps.

That said, I'm open to not including the deps version information at all if people feel strongly against it.
achilleas-k: > That said, I'm open to not including the deps version information at all if people feel strongly against it.

I don't feel strongly about it, no.  If you get it to work and it's reliable, let's keep it.  But if it ends up taking too much time to get it to work in all scenarios, I wanted to make sure it was necessary.

Patch:
diff --git a/cmd/osbuild-koji/main.go b/cmd/osbuild-koji/main.go
index a2f807f49..26dbc84e7 100644
--- a/cmd/osbuild-koji/main.go
+++ b/cmd/osbuild-koji/main.go
@@ -98,8 +98,8 @@ func main() {
 			Checksum:     hash,
 			Type:         koji.BuildOutputTypeImage,
 			RPMs:         []rpmmd.RPM{},
-			Extra: koji.BuildOutputExtra{
-				Image: koji.ImageExtraInfo{
+			Extra: &koji.BuildOutputExtra{
+				ImageOutput: koji.ImageExtraInfo{
 					Arch:     arch,
 					BootMode: distro.BOOT_NONE.String(), // TODO: put the correct boot mode here
 				},
diff --git a/cmd/osbuild-worker/jobimpl-koji-finalize.go b/cmd/osbuild-worker/jobimpl-koji-finalize.go
index 61ea97a52..c0332de26 100644
--- a/cmd/osbuild-worker/jobimpl-koji-finalize.go
+++ b/cmd/osbuild-worker/jobimpl-koji-finalize.go
@@ -119,6 +119,7 @@ func (impl *KojiFinalizeJobImpl) Run(job worker.Job) error {
 	var outputs []koji.BuildOutput
 	// Extra info for each image output is stored using the image filename as the key
 	imgOutputsExtraInfo := map[string]koji.ImageExtraInfo{}
+	manifestOutputsExtraInfo := map[string]*koji.ManifestExtraInfo{}
 
 	var osbuildResults []worker.OSBuildJobResult
 	initArgs, osbuildResults, err = extractDynamicArgs(job)
@@ -133,11 +134,11 @@ func (impl *KojiFinalizeJobImpl) Run(job worker.Job) error {
 		return nil
 	}
 
-	for i, buildArgs := range osbuildResults {
+	for i, buildResult := range osbuildResults {
 		buildRPMs := make([]rpmmd.RPM, 0)
 		// collect packages from stages in build pipelines
-		for _, plName := range buildArgs.PipelineNames.Build {
-			buildPipelineMd := buildArgs.OSBuildOutput.Metadata[plName]
+		for _, plName := range buildResult.PipelineNames.Build {
+			buildPipelineMd := buildResult.OSBuildOutput.Metadata[plName]
 			buildRPMs = append(buildRPMs, osbuild.OSBuildMetadataToRPMs(buildPipelineMd)...)
 		}
 		// this dedupe is usually not necessary since we generally only have
@@ -145,7 +146,7 @@ func (impl *KojiFinalizeJobImpl) Run(job worker.Job) error {
 		// multiple
 		buildRPMs = rpmmd.DeduplicateRPMs(buildRPMs)
 
-		kojiTargetResults := buildArgs.TargetResultsByName(target.TargetNameKoji)
+		kojiTargetResults := buildResult.TargetResultsByName(target.TargetNameKoji)
 		// Only a single Koji target is allowed per osbuild job
 		if len(kojiTargetResults) != 1 {
 			kojiFinalizeJobResult.JobError = clienterrors.WorkerClientError(clienterrors.ErrorKojiFinalize, "Exactly one Koji target result is expected per osbuild job", nil)
@@ -158,16 +159,16 @@ func (impl *KojiFinalizeJobImpl) Run(job worker.Job) error {
 		buildRoots = append(buildRoots, koji.BuildRoot{
 			ID: uint64(i),
 			Host: koji.Host{
-				Os:   buildArgs.HostOS,
-				Arch: buildArgs.Arch,
+				Os:   buildResult.HostOS,
+				Arch: buildResult.Arch,
 			},
 			ContentGenerator: koji.ContentGenerator{
 				Name:    "osbuild",
-				Version: "0", // TODO: put the correct version here
+				Version: buildResult.OSBuildVersion,
 			},
 			Container: koji.Container{
 				Type: "none",
-				Arch: buildArgs.Arch,
+				Arch: buildResult.Arch,
 			},
 			Tools: []koji.Tool{},
 			RPMs:  buildRPMs,
@@ -175,8 +176,8 @@ func (impl *KojiFinalizeJobImpl) Run(job worker.Job) error {
 
 		// collect packages from stages in payload pipelines
 		imageRPMs := make([]rpmmd.RPM, 0)
-		for _, plName := range buildArgs.PipelineNames.Payload {
-			payloadPipelineMd := buildArgs.OSBuildOutput.Metadata[plName]
+		for _, plName := range buildResult.PipelineNames.Payload {
+			payloadPipelineMd := buildResult.OSBuildOutput.Metadata[plName]
 			imageRPMs = append(imageRPMs, osbuild.OSBuildMetadataToRPMs(payloadPipelineMd)...)
 		}
 
@@ -184,24 +185,102 @@ func (impl *KojiFinalizeJobImpl) Run(job worker.Job) error {
 		imageRPMs = rpmmd.DeduplicateRPMs(imageRPMs)
 
 		imgOutputExtraInfo := koji.ImageExtraInfo{
-			Arch:     buildArgs.Arch,
-			BootMode: buildArgs.ImageBootMode,
+			Arch:            buildResult.Arch,
+			BootMode:        buildResult.ImageBootMode,
+			OSBuildArtifact: kojiTargetResult.OsbuildArtifact,
+			OSBuildVersion:  buildResult.OSBuildVersion,
 		}
-		imgOutputsExtraInfo[args.KojiFilenames[i]] = imgOutputExtraInfo
 
+		// The image filename is now set in the KojiTargetResultOptions.
+		// For backward compatibility, if the filename is not set in the
+		// options, use the filename from the KojiTargetOptions.
+		imageFilename := kojiTargetOptions.Image.Filename
+		if imageFilename == "" {
+			imageFilename = args.KojiFilenames[i]
+		}
+
+		// If there are any non-Koji target results in the build,
+		// add them to the image output extra metadata.
+		nonKojiTargetResults := buildResult.TargetResultsFilterByName([]target.TargetName{target.TargetNameKoji})
+		if len(nonKojiTargetResults) > 0 {
+			imgOutputExtraInfo.UploadTargetResults = nonKojiTargetResults
+		}
+
+		imgOutputsExtraInfo[imageFilename] = imgOutputExtraInfo
+
+		// Image output
 		outputs = append(outputs, koji.BuildOutput{
 			BuildRootID:  uint64(i),
-			Filename:     args.KojiFilenames[i],
-			FileSize:     kojiTargetOptions.ImageSize,
-			Arch:         buildArgs.Arch,
-			ChecksumType: koji.ChecksumTypeMD5,
-			Checksum:     kojiTargetOptions.ImageMD5,
+			Filename:     imageFilename,
+			FileSize:     kojiTargetOptions.Image.Size,
+			Arch:         buildResult.Arch,
+			ChecksumType: koji.ChecksumType(kojiTargetOptions.Image.ChecksumType),
+			Checksum:     kojiTargetOptions.Image.Checksum,
 			Type:         koji.BuildOutputTypeImage,
 			RPMs:         imageRPMs,
-			Extra: koji.BuildOutputExtra{
-				Image: imgOutputExtraInfo,
+			Extra: &koji.BuildOutputExtra{
+				ImageOutput: imgOutputExtraInfo,
 			},
 		})
+
+		// OSBuild manifest output
+		// TODO: Condition below is present for backward compatibility with old workers which don't upload the manifest.
+		// TODO: Remove the condition it in the future.
+		if kojiTargetOptions.OSBuildManifest != nil {
+			manifestExtraInfo := koji.ManifestExtraInfo{
+				Arch: buildResult.Arch,
+			}
+
+			if kojiTargetOptions.OSBuildManifestInfo != nil {
+				manifestInfo := &koji.ManifestInfo{
+					OSBuildComposerVersion: kojiTargetOptions.OSBuildManifestInfo.OSBuildComposerVersion,
+				}
+				for _, composerDep := range kojiTargetOptions.OSBuildManifestInfo.OSBuildComposerDeps {
+					dep := &koji.OSBuildComposerDepModule{
+						Path:    composerDep.Path,
+						Version: composerDep.Version,
+					}
+					if composerDep.Replace != nil {
+						dep.Replace = &koji.OSBuildComposerDepModule{
+							Path:    composerDep.Replace.Path,
+							Version: composerDep.Replace.Version,
+						}
+					}
+					manifestInfo.OSBuildComposerDeps = append(manifestInfo.OSBuildComposerDeps, dep)
+				}
+				manifestExtraInfo.Info = manifestInfo
+			}
+
+			manifestOutputsExtraInfo[kojiTargetOptions.OSBuildManifest.Filename] = &manifestExtraInfo
+
+			outputs = append(outputs, koji.BuildOutput{
+				BuildRootID:  uint64(i),
+				Filename:     kojiTargetOptions.OSBuildManifest.Filename,
+				FileSize:     kojiTargetOptions.OSBuildManifest.Size,
+				Arch:         buildResult.Arch,
+				ChecksumType: koji.ChecksumType(kojiTargetOptions.OSBuildManifest.ChecksumType),
+				Checksum:     kojiTargetOptions.OSBuildManifest.Checksum,
+				Type:         koji.BuildOutputTypeManifest,
+				Extra: &koji.BuildOutputExtra{
+					ImageOutput: manifestExtraInfo,
+				},
+			})
+		}
+
+		// Build log output
+		// TODO: Condition below is present for backward compatibility with old workers which don't upload the log.
+		// TODO: Remove the condition it in the future.
+		if kojiTargetOptions.Log != nil {
+			outputs = append(outputs, koji.BuildOutput{
+				BuildRootID:  uint64(i),
+				Filename:     kojiTargetOptions.Log.Filename,
+				FileSize:     kojiTargetOptions.Log.Size,
+				Arch:         "noarch", // log file is not architecture dependent
+				ChecksumType: koji.ChecksumType(kojiTargetOptions.Log.ChecksumType),
+				Checksum:     kojiTargetOptions.Log.Checksum,
+				Type:         koji.BuildOutputTypeLog,
+			})
+		}
 	}
 
 	build := koji.Build{
@@ -213,9 +292,10 @@ func (impl *KojiFinalizeJobImpl) Run(job worker.Job) error {
 		StartTime: int64(args.StartTime),
 		EndTime:   time.Now().Unix(),
 		Extra: koji.BuildExtra{
-			TypeInfo: koji.TypeInfo{
+			TypeInfo: koji.TypeInfoBuild{
 				Image: imgOutputsExtraInfo,
 			},
+			Manifest: manifestOutputsExtraInfo,
 		},
 	}
 
diff --git a/cmd/osbuild-worker/jobimpl-osbuild.go b/cmd/osbuild-worker/jobimpl-osbuild.go
index 3625fc0b8..13e669af5 100644
--- a/cmd/osbuild-worker/jobimpl-osbuild.go
+++ b/cmd/osbuild-worker/jobimpl-osbuild.go
@@ -1,6 +1,7 @@
 package main
 
 import (
+	"bytes"
 	"context"
 	"crypto/rand"
 	"fmt"
@@ -340,6 +341,13 @@ func (impl *OSBuildJobImpl) Run(job worker.Job) error {
 		}
 	}()
 
+	osbuildVersion, err := osbuild.OSBuildVersion()
+	if err != nil {
+		osbuildJobResult.JobError = clienterrors.WorkerClientError(clienterrors.ErrorBuildJob, "Error getting osbuild binary version", err)
+		return err
+	}
+	osbuildJobResult.OSBuildVersion = osbuildVersion
+
 	// Read the job specification
 	var jobArgs worker.OSBuildJob
 	err = job.Args(&jobArgs)
@@ -348,6 +356,7 @@ func (impl *OSBuildJobImpl) Run(job worker.Job) error {
 	}
 
 	// In case the manifest is empty, try to get it from dynamic args
+	var manifestInfo *worker.ManifestInfo
 	if len(jobArgs.Manifest) == 0 {
 		if job.NDynamicArgs() > 0 {
 			var manifestJR worker.ManifestJobByIDResult
@@ -373,6 +382,7 @@ func (impl *OSBuildJobImpl) Run(job worker.Job) error {
 				return nil
 			}
 			jobArgs.Manifest = manifestJR.Manifest
+			manifestInfo = &manifestJR.ManifestInfo
 		}
 
 		if len(jobArgs.Manifest) == 0 {
@@ -457,7 +467,7 @@ func (impl *OSBuildJobImpl) Run(job worker.Job) error {
 		var targetResult *target.TargetResult
 		switch targetOptions := jobTarget.Options.(type) {
 		case *target.WorkerServerTargetOptions:
-			targetResult = target.NewWorkerServerTargetResult()
+			targetResult = target.NewWorkerServerTargetResult(&jobTarget.OsbuildArtifact)
 			var f *os.File
 			imagePath := path.Join(outputDirectory, jobTarget.OsbuildArtifact.ExportName, jobTarget.OsbuildArtifact.ExportFilename)
 			f, err = os.Open(imagePath)
@@ -473,7 +483,7 @@ func (impl *OSBuildJobImpl) Run(job worker.Job) error {
 			}
 
 		case *target.VMWareTargetOptions:
-			targetResult = target.NewVMWareTargetResult()
+			targetResult = target.NewVMWareTargetResult(&jobTarget.OsbuildArtifact)
 			credentials := vmware.Credentials{
 				Username:   targetOptions.Username,
 				Password:   targetOptions.Password,
@@ -527,7 +537,7 @@ func (impl *OSBuildJobImpl) Run(job worker.Job) error {
 			}
 
 		case *target.AWSTargetOptions:
-			targetResult = target.NewAWSTargetResult(nil)
+			targetResult = target.NewAWSTargetResult(nil, &jobTarget.OsbuildArtifact)
 			a, err := impl.getAWS(targetOptions.Region, targetOptions.AccessKeyID, targetOptions.SecretAccessKey, targetOptions.SessionToken)
 			if err != nil {
 				targetResult.TargetError = clienterrors.WorkerClientError(clienterrors.ErrorInvalidConfig, err.Error(), nil)
@@ -584,7 +594,7 @@ func (impl *OSBuildJobImpl) Run(job worker.Job) error {
 			}
 
 		case *target.AWSS3TargetOptions:
-			targetResult = target.NewAWSS3TargetResult(nil)
+			targetResult = target.NewAWSS3TargetResult(nil, &jobTarget.OsbuildArtifact)
 			a, bucket, err := impl.getAWSForS3Target(targetOptions)
 			if err != nil {
 				targetResult.TargetError = clienterrors.WorkerClientError(clienterrors.ErrorInvalidConfig, err.Error(), nil)
@@ -604,7 +614,7 @@ func (impl *OSBuildJobImpl) Run(job worker.Job) error {
 			targetResult.Options = &target.AWSS3TargetResultOptions{URL: url}
 
 		case *target.AzureTargetOptions:
-			targetResult = target.NewAzureTargetResult()
+			targetResult = target.NewAzureTargetResult(&jobTarget.OsbuildArtifact)
 			azureStorageClient, err := azure.NewStorageClient(targetOptions.StorageAccount, targetOptions.StorageAccessKey)
 			if err != nil {
 				targetResult.TargetError = clienterrors.WorkerClientError(clienterrors.ErrorInvalidConfig, err.Error(), nil)
@@ -632,7 +642,7 @@ func (impl *OSBuildJobImpl) Run(job worker.Job) error {
 			}
 
 		case *target.GCPTargetOptions:
-			targetResult = target.NewGCPTargetResult(nil)
+			targetResult = target.NewGCPTargetResult(nil, &jobTarget.OsbuildArtifact)
 			ctx := context.Background()
 
 			g, err := impl.getGCP(targetOptions.Credentials)
@@ -697,7 +707,7 @@ func (impl *OSBuildJobImpl) Run(job worker.Job) error {
 			}
 
 		case *target.AzureImageTargetOptions:
-			targetResult = target.NewAzureImageTargetResult(nil)
+			targetResult = target.NewAzureImageTargetResult(nil, &jobTarget.OsbuildArtifact)
 			ctx := context.Background()
 
 			if impl.AzureConfig.Creds == nil {
@@ -827,7 +837,7 @@ func (impl *OSBuildJobImpl) Run(job worker.Job) error {
 			}
 
 		case *target.KojiTargetOptions:
-			targetResult = target.NewKojiTargetResult(nil)
+			targetResult = target.NewKojiTargetResult(nil, &jobTarget.OsbuildArtifact)
 			kojiServerURL, err := url.Parse(targetOptions.Server)
 			if err != nil {
 				targetResult.TargetError = clienterrors.WorkerClientError(clienterrors.ErrorInvalidTargetConfig, fmt.Sprintf("failed to parse Koji server URL: %v", err), nil)
@@ -871,13 +881,81 @@ func (impl *OSBuildJobImpl) Run(job worker.Job) error {
 				break
 			}
 			logWithId.Info("[Koji] 🎉 Image successfully uploaded")
+
+			manifest := bytes.NewReader(jobArgs.Manifest)
+			logWithId.Info("[Koji] ⬆ Uploading the osbuild manifest")
+			manifestFilename := jobTarget.ImageName + ".manifest.json"
+			manifestHash, manifestSize, err := kojiAPI.Upload(manifest, targetOptions.UploadDirectory, manifestFilename)
+			if err != nil {
+				logWithId.Warnf("[Koji] ⬆ upload failed: %v", err)
+				targetResult.TargetError = clienterrors.WorkerClientError(clienterrors.ErrorUploadingImage, err.Error(), nil)
+				break
+			}
+			logWithId.Info("[Koji] 🎉 Manifest successfully uploaded")
+
+			var osbuildLog bytes.Buffer
+			err = osbuildJobResult.OSBuildOutput.Write(&osbuildLog)
+			if err != nil {
+				logWithId.Warnf("[Koji] Converting osbuild log to texrt failed: %v", err)
+				targetResult.TargetError = clienterrors.WorkerClientError(clienterrors.ErrorKojiBuild, err.Error(), nil)
+				break
+			}
+			logWithId.Info("[Koji] ⬆ Uploading the osbuild output log")
+			osbuildOutputFilename := jobTarget.ImageName + ".osbuild.log"
+			osbuildOutputHash, osbuildOutputSize, err := kojiAPI.Upload(&osbuildLog, targetOptions.UploadDirectory, osbuildOutputFilename)
+			if err != nil {
+				logWithId.Warnf("[Koji] ⬆ upload failed: %v", err)
+				targetResult.TargetError = clienterrors.WorkerClientError(clienterrors.ErrorUploadingImage, err.Error(), nil)
+				break
+			}
+			logWithId.Info("[Koji] 🎉 osbuild output log successfully uploaded")
+
+			// Attach the manifest info to the koji target result, so that it
+			// it can be imported to the Koji build by the koji-finalize job.
+			var kojiManifestInfo *target.ManifestInfo
+			if manifestInfo != nil {
+				kojiManifestInfo = &target.ManifestInfo{
+					OSBuildComposerVersion: manifestInfo.OSBuildComposerVersion,
+				}
+				for _, composerDep := range manifestInfo.OSBuildComposerDeps {
+					dep := &target.OSBuildComposerDepModule{
+						Path:    composerDep.Path,
+						Version: composerDep.Version,
+					}
+					if composerDep.Replace != nil {
+						dep.Replace = &target.OSBuildComposerDepModule{
+							Path:    composerDep.Replace.Path,
+							Version: composerDep.Replace.Version,
+						}
+					}
+					kojiManifestInfo.OSBuildComposerDeps = append(kojiManifestInfo.OSBuildComposerDeps, dep)
+				}
+			}
+
 			targetResult.Options = &target.KojiTargetResultOptions{
-				ImageMD5:  imageHash,
-				ImageSize: imageSize,
+				Image: &target.KojiOutputInfo{
+					Filename:     jobTarget.ImageName,
+					ChecksumType: target.ChecksumTypeMD5,
+					Checksum:     imageHash,
+					Size:         imageSize,
+				},
+				OSBuildManifest: &target.KojiOutputInfo{
+					Filename:     manifestFilename,
+					ChecksumType: target.ChecksumTypeMD5,
+					Checksum:     manifestHash,
+					Size:         manifestSize,
+				},
+				Log: &target.KojiOutputInfo{
+					Filename:     osbuildOutputFilename,
+					ChecksumType: target.ChecksumTypeMD5,
+					Checksum:     osbuildOutputHash,
+					Size:         osbuildOutputSize,
+				},
+				OSBuildManifestInfo: kojiManifestInfo,
 			}
 
 		case *target.OCITargetOptions:
-			targetResult = target.NewOCITargetResult(nil)
+			targetResult = target.NewOCITargetResult(nil, &jobTarget.OsbuildArtifact)
 			// create an ociClient uploader with a valid storage client
 			var ociClient oci.Client
 			ociClient, err = impl.getOCI(oci.ClientParams{
@@ -938,7 +1016,7 @@ func (impl *OSBuildJobImpl) Run(job worker.Job) error {
 			logWithId.Info("[OCI] 🎉 Image uploaded and registered!")
 			targetResult.Options = &target.OCITargetResultOptions{ImageID: imageID}
 		case *target.OCIObjectStorageTargetOptions:
-			targetResult = target.NewOCIObjectStorageTargetResult(nil)
+			targetResult = target.NewOCIObjectStorageTargetResult(nil, &jobTarget.OsbuildArtifact)
 			// create an ociClient uploader with a valid storage client
 			ociClient, err := impl.getOCI(oci.ClientParams{
 				User:        targetOptions.User,
@@ -987,7 +1065,7 @@ func (impl *OSBuildJobImpl) Run(job worker.Job) error {
 			logWithId.Info("[OCI] 🎉 Image uploaded and pre-authenticated request generated!")
 			targetResult.Options = &target.OCIObjectStorageTargetResultOptions{URL: uri}
 		case *target.ContainerTargetOptions:
-			targetResult = target.NewContainerTargetResult(nil)
+			targetResult = target.NewContainerTargetResult(nil, &jobTarget.OsbuildArtifact)
 			destination := jobTarget.ImageName
 
 			logWithId.Printf("[container] 📦 Preparing upload to '%s'", destination)
diff --git a/go.mod b/go.mod
index 38394041b..92e98c606 100644
--- a/go.mod
+++ b/go.mod
@@ -12,7 +12,7 @@ require (
 	github.com/Azure/go-autorest/autorest v0.11.29
 	github.com/Azure/go-autorest/autorest/azure/auth v0.5.12
 	github.com/BurntSushi/toml v1.3.2
-	github.com/aws/aws-sdk-go v1.45.16
+	github.com/aws/aws-sdk-go v1.45.17
 	github.com/coreos/go-semver v0.3.1
 	github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f
 	github.com/deepmap/oapi-codegen v1.8.2
diff --git a/go.sum b/go.sum
index 24b371fec..ed7f50f3e 100644
--- a/go.sum
+++ b/go.sum
@@ -98,8 +98,8 @@ github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk5
 github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
 github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
 github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
-github.com/aws/aws-sdk-go v1.45.16 h1:spca2z7UJgoQ5V2fX6XiHDCj2E65kOJAfbUPozSkE24=
-github.com/aws/aws-sdk-go v1.45.16/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI=
+github.com/aws/aws-sdk-go v1.45.17 h1:EclP31IdsIVaHNgIlUtvFIDuyFUtTCa9oYthZmGlZ1U=
+github.com/aws/aws-sdk-go v1.45.17/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI=
 github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
 github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
 github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
diff --git a/internal/cloudapi/v2/server.go b/internal/cloudapi/v2/server.go
index 50442c7b2..f522b625f 100644
--- a/internal/cloudapi/v2/server.go
+++ b/internal/cloudapi/v2/server.go
@@ -414,6 +414,20 @@ func serializeManifest(ctx context.Context, manifestSource *manifest.Manifest, w
 
 	jobResult := &worker.ManifestJobByIDResult{
 		Manifest: nil,
+		ManifestInfo: worker.ManifestInfo{
+			OSBuildComposerVersion: common.BuildVersion(),
+		},
+	}
+
+	// add osbuild/images dependency info to job result
+	osbuildImagesDep, err := common.GetDepModuleInfoByPath(common.OSBuildImagesModulePath)
+	if err != nil {
+		// do not fail here and just log the error, because the module info is not available in tests.
+		// Failing here would make the unit tests fail. See https://github.com/golang/go/issues/33976
+		logWithId.Errorf("Error getting %s dependency info: %v", common.OSBuildImagesModulePath, err)
+	} else {
+		osbuildImagesDepModule := worker.ComposerDepModuleFromDebugModule(osbuildImagesDep)
+		jobResult.ManifestInfo.OSBuildComposerDeps = append(jobResult.ManifestInfo.OSBuildComposerDeps, osbuildImagesDepModule)
 	}
 
 	defer func() {
diff --git a/internal/common/dependencies.go b/internal/common/dependencies.go
new file mode 100644
index 000000000..372ef2f19
--- /dev/null
+++ b/internal/common/dependencies.go
@@ -0,0 +1,28 @@
+package common
+
+import (
+	"fmt"
+	"runtime/debug"
+)
+
+const (
+	OSBuildImagesModulePath = "github.com/osbuild/images"
+)
+
+// GetDepModuleInfoByPath returns the debug.Module for the dependency
+// with the given path. If the dependency is not found, an error is
+// returned.
+func GetDepModuleInfoByPath(path string) (*debug.Module, error) {
+	buildinfo, ok := debug.ReadBuildInfo()
+	if !ok {
+		return nil, fmt.Errorf("Failed to read build info")
+	}
+
+	for _, dep := range buildinfo.Deps {
+		if dep.Path == path {
+			return dep, nil
+		}
+	}
+
+	return nil, fmt.Errorf("Could not find dependency %s", path)
+}
diff --git a/internal/target/aws_target.go b/internal/target/aws_target.go
index 1f82bbf00..b6921a1a4 100644
--- a/internal/target/aws_target.go
+++ b/internal/target/aws_target.go
@@ -37,8 +37,8 @@ type AWSTargetResultOptions struct {
 
 func (AWSTargetResultOptions) isTargetResultOptions() {}
 
-func NewAWSTargetResult(options *AWSTargetResultOptions) *TargetResult {
-	return newTargetResult(TargetNameAWS, options)
+func NewAWSTargetResult(options *AWSTargetResultOptions, artifact *OsbuildArtifact) *TargetResult {
+	return newTargetResult(TargetNameAWS, options, artifact)
 }
 
 type AWSS3TargetOptions struct {
@@ -66,6 +66,6 @@ type AWSS3TargetResultOptions struct {
 
 func (AWSS3TargetResultOptions) isTargetResultOptions() {}
 
-func NewAWSS3TargetResult(options *AWSS3TargetResultOptions) *TargetResult {
-	return newTargetResult(TargetNameAWSS3, options)
+func NewAWSS3TargetResult(options *AWSS3TargetResultOptions, artifact *OsbuildArtifact) *TargetResult {
+	return newTargetResult(TargetNameAWSS3, options, artifact)
 }
diff --git a/internal/target/azure_image_target.go b/internal/target/azure_image_target.go
index c5b5c713d..19e70cf4b 100644
--- a/internal/target/azure_image_target.go
+++ b/internal/target/azure_image_target.go
@@ -39,6 +39,6 @@ type AzureImageTargetResultOptions struct {
 
 func (AzureImageTargetResultOptions) isTargetResultOptions() {}
 
-func NewAzureImageTargetResult(options *AzureImageTargetResultOptions) *TargetResult {
-	return newTargetResult(TargetNameAzureImage, options)
+func NewAzureImageTargetResult(options *AzureImageTargetResultOptions, artifact *OsbuildArtifact) *TargetResult {
+	return newTargetResult(TargetNameAzureImage, options, artifact)
 }
diff --git a/internal/target/azure_target.go b/internal/target/azure_target.go
index 1cb9dd65e..931d5adc0 100644
--- a/internal/target/azure_target.go
+++ b/internal/target/azure_target.go
@@ -24,6 +24,6 @@ func NewAzureTarget(options *AzureTargetOptions) *Target {
 	return newTarget(TargetNameAzure, options)
 }
 
-func NewAzureTargetResult() *TargetResult {
-	return newTargetResult(TargetNameAzure, nil)
+func NewAzureTargetResult(artifact *OsbuildArtifact) *TargetResult {
+	return newTargetResult(TargetNameAzure, nil, artifact)
 }
diff --git a/internal/target/container_target.go b/internal/target/container_target.go
index 237718a7a..a0c2956c3 100644
--- a/internal/target/container_target.go
+++ b/internal/target/container_target.go
@@ -24,6 +24,6 @@ type ContainerTargetResultOptions struct {
 
 func (ContainerTargetResultOptions) isTargetResultOptions() {}
 
-func NewContainerTargetResult(options *ContainerTargetResultOptions) *TargetResult {
-	return newTargetResult(TargetNameContainer, options)
+func NewContainerTargetResult(options *ContainerTargetResultOptions, artifact *OsbuildArtifact) *TargetResult {
+	return newTargetResult(TargetNameContainer, options, artifact)
 }
diff --git a/internal/target/gcp_target.go b/internal/target/gcp_target.go
index 3979f00e8..5d43e7030 100644
--- a/internal/target/gcp_target.go
+++ b/internal/target/gcp_target.go
@@ -28,6 +28,6 @@ type GCPTargetResultOptions struct {
 
 func (GCPTargetResultOptions) isTargetResultOptions() {}
 
-func NewGCPTargetResult(options *GCPTargetResultOptions) *TargetResult {
-	return newTargetResult(TargetNameGCP, options)
+func NewGCPTargetResult(options *GCPTargetResultOptions, artifact *OsbuildArtifact) *TargetResult {
+	return newTargetResult(TargetNameGCP, options, artifact)
 }
diff --git a/internal/target/koji_target.go b/internal/target/koji_target.go
index 72e58de4d..e24f8344e 100644
--- a/internal/target/koji_target.go
+++ b/internal/target/koji_target.go
@@ -1,5 +1,10 @@
 package target
 
+import (
+	"encoding/json"
+	"fmt"
+)
+
 const TargetNameKoji TargetName = "org.osbuild.koji"
 
 type KojiTargetOptions struct {
@@ -13,13 +18,121 @@ func NewKojiTarget(options *KojiTargetOptions) *Target {
 	return newTarget(TargetNameKoji, options)
 }
 
+// ChecksumType represents the type of a checksum used for a KojiOutputInfo.
+type ChecksumType string
+
+const (
+	ChecksumTypeMD5 ChecksumType = "md5"
+
+	// Only MD5 is supported for now to enable backwards compatibility.
+	// The reason is tha the old KojiTargetOptions contained only
+	// ImageMD5 and ImageSize fields, which mandates the use of MD5.
+	// TODO: uncomment the lines below when the backwards compatibility is no longer needed.
+	//ChecksumTypeAdler32 ChecksumType = "adler32"
+	//ChecksumTypeSHA256  ChecksumType = "sha256"
+)
+
+// KojiOutputInfo represents the information about any output file uploaded to Koji
+// as part of the OSBuild job. This information is then used by the KojiFinalize
+// job when importing files into Koji.
+type KojiOutputInfo struct {
+	Filename     string       `json:"filename"`
+	ChecksumType ChecksumType `json:"checksum_type"`
+	Checksum     string       `json:"checksum"`
+	Size         uint64       `json:"size"`
+}
+
+type OSBuildComposerDepModule struct {
+	Path    string                    `json:"path"`
+	Version string                    `json:"version"`
+	Replace *OSBuildComposerDepModule `json:"replace,omitempty"`
+}
+
+// ManifestInfo contains information about the environment in which
+// the manifest was produced and which could affect its content.
+type ManifestInfo struct {
+	OSBuildComposerVersion string `json:"osbuild_composer_version"`
+	// List of relevant modules used by osbuild-composer which
+	// could affect the manifest content.
+	OSBuildComposerDeps []*OSBuildComposerDepModule `json:"osbuild_composer_deps,omitempty"`
+}
+
 type KojiTargetResultOptions struct {
-	ImageMD5  string `json:"image_md5"`
-	ImageSize uint64 `json:"image_size"`
+	Image               *KojiOutputInfo `json:"image"`
+	Log                 *KojiOutputInfo `json:"log,omitempty"`
+	OSBuildManifest     *KojiOutputInfo `json:"osbuild_manifest,omitempty"`
+	OSBuildManifestInfo *ManifestInfo   `json:"osbuild_manifest_info,omitempty"`
+}
+
+func (o *KojiTargetResultOptions) UnmarshalJSON(data []byte) error {
+	type aliasType KojiTargetResultOptions
+	if err := json.Unmarshal(data, (*aliasType)(o)); err != nil {
+		return err
+	}
+
+	// compatType contains deprecated fields, which are being checked
+	// for backwards compatibility.
+	type compatType struct {
+		// Deprecated: Use Image in KojiTargetOptions instead.
+		// Kept for backwards compatibility.
+		ImageMD5  string `json:"image_md5"`
+		ImageSize uint64 `json:"image_size"`
+	}
+
+	var compat compatType
+	if err := json.Unmarshal(data, &compat); err != nil {
+		return err
+	}
+
+	// Check if the Image data in the new struct format are set.
+	// If not, then the data are coming from an old composer.
+	if o.Image == nil {
+		// o.Image.Filename is kept empty, because the filename was previously
+		// not set as there was always only the Image file. The KojiFinalize job
+		// handles this case and takes the Image filename from the KojiFinalizeJob
+		// options.
+
+		o.Image = &KojiOutputInfo{
+			ChecksumType: ChecksumTypeMD5,
+			Checksum:     compat.ImageMD5,
+			Size:         compat.ImageSize,
+		}
+	}
+
+	return nil
+}
+
+func (o KojiTargetResultOptions) MarshalJSON() ([]byte, error) {
+	type alias KojiTargetResultOptions
+	// compatType is a super-set of the current KojiTargetResultOptions and
+	// old version of it. It contains deprecated fields, which are being set
+	// for backwards compatibility.
+	type compatType struct {
+		alias
+
+		// Deprecated: Use Image in KojiTargetOptions instead.
+		// Kept for backwards compatibility.
+		ImageMD5  string `json:"image_md5"`
+		ImageSize uint64 `json:"image_size"`
+	}
+
+	// Only MD5 is supported for now to enable backwards compatibility.
+	// TODO: remove this block when the backwards compatibility is no longer needed.
+	if o.Image.ChecksumType != ChecksumTypeMD5 {
+		return nil, fmt.Errorf("unsupported checksum type: %s", o.Image.ChecksumType)
+	}
+
+	compat := compatType{
+		alias:     (alias)(o),
+		ImageMD5:  o.Image.Checksum,
+		ImageSize: o.Image.Size,
+	}
+
+	return json.Marshal(compat)
 }
 
 func (KojiTargetResultOptions) isTargetResultOptions() {}
 
-func NewKojiTargetResult(options *KojiTargetResultOptions) *TargetResult {
-	return newTargetResult(TargetNameKoji, options)
+func NewKojiTargetResult(options *KojiTargetResultOptions, artifact *OsbuildArtifact) *TargetResult {
+	return newTargetResult(TargetNameKoji, options, artifact)
 }
diff --git a/internal/target/oci_target.go b/internal/target/oci_target.go
index 5759332de..08c891e20 100644
--- a/internal/target/oci_target.go
+++ b/internal/target/oci_target.go
@@ -26,8 +26,8 @@ type OCITargetResultOptions struct {
 
 func (OCITargetResultOptions) isTargetResultOptions() {}
 
-func NewOCITargetResult(options *OCITargetResultOptions) *TargetResult {
-	return newTargetResult(TargetNameOCI, options)
+func NewOCITargetResult(options *OCITargetResultOptions, artifact *OsbuildArtifact) *TargetResult {
+	return newTargetResult(TargetNameOCI, options, artifact)
 }
 
 const TargetNameOCIObjectStorage TargetName = "org.osbuild.oci.objectstorage"
@@ -55,6 +55,6 @@ type OCIObjectStorageTargetResultOptions struct {
 
 func (OCIObjectStorageTargetResultOptions) isTargetResultOptions() {}
 
-func NewOCIObjectStorageTargetResult(options *OCIObjectStorageTargetResultOptions) *TargetResult {
-	return newTargetResult(TargetNameOCIObjectStorage, options)
+func NewOCIObjectStorageTargetResult(options *OCIObjectStorageTargetResultOptions, artifact *OsbuildArtifact) *TargetResult {
+	return newTargetResult(TargetNameOCIObjectStorage, options, artifact)
 }
diff --git a/internal/target/targetresult.go b/internal/target/targetresult.go
index 0fd1922d8..59bccbfb7 100644
--- a/internal/target/targetresult.go
+++ b/internal/target/targetresult.go
@@ -8,15 +8,18 @@ import (
 )
 
 type TargetResult struct {
-	Name        TargetName          `json:"name"`
-	Options     TargetResultOptions `json:"options,omitempty"`
-	TargetError *clienterrors.Error `json:"target_error,omitempty"`
+	Name    TargetName          `json:"name"`
+	Options TargetResultOptions `json:"options,omitempty"`
+	// Configuration used to produce osbuild artifact specific to this target
+	OsbuildArtifact *OsbuildArtifact    `json:"osbuild_artifact,omitempty"`
+	TargetError     *clienterrors.Error `json:"target_error,omitempty"`
 }
 
-func newTargetResult(name TargetName, options TargetResultOptions) *TargetResult {
+func newTargetResult(name TargetName, options TargetResultOptions, artifact *OsbuildArtifact) *TargetResult {
 	return &TargetResult{
-		Name:    name,
-		Options: options,
+		Name:            name,
+		Options:         options,
+		OsbuildArtifact: artifact,
 	}
 }
 
@@ -25,9 +28,10 @@ type TargetResultOptions interface {
 }
 
 type rawTargetResult struct {
-	Name        TargetName          `json:"name"`
-	Options     json.RawMessage     `json:"options,omitempty"`
-	TargetError *clienterrors.Error `json:"target_error,omitempty"`
+	Name            TargetName          `json:"name"`
+	Options         json.RawMessage     `json:"options,omitempty"`
+	OsbuildArtifact *OsbuildArtifact    `json:"osbuild_artifact,omitempty"`
+	TargetError     *clienterrors.Error `json:"target_error,omitempty"`
 }
 
 func (targetResult *TargetResult) UnmarshalJSON(data []byte) error {
@@ -48,6 +52,7 @@ func (targetResult *TargetResult) UnmarshalJSON(data []byte) error {
 
 	targetResult.Name = rawTR.Name
 	targetResult.Options = options
+	targetResult.OsbuildArtifact = rawTR.OsbuildArtifact
 	targetResult.TargetError = rawTR.TargetError
 	return nil
 }
diff --git a/internal/target/vmware_target.go b/internal/target/vmware_target.go
index 594ab1cf0..f7d415dd9 100644
--- a/internal/target/vmware_target.go
+++ b/internal/target/vmware_target.go
@@ -18,6 +18,6 @@ func NewVMWareTarget(options *VMWareTargetOptions) *Target {
 	return newTarget(TargetNameVMWare, options)
 }
 
-func NewVMWareTargetResult() *TargetResult {
-	return newTargetResult(TargetNameVMWare, nil)
+func NewVMWareTargetResult(artifact *OsbuildArtifact) *TargetResult {
+	return newTargetResult(TargetNameVMWare, nil, artifact)
 }
diff --git a/internal/target/worker_server_target.go b/internal/target/worker_server_target.go
index 880aea414..e25c5c48e 100644
--- a/internal/target/worker_server_target.go
+++ b/internal/target/worker_server_target.go
@@ -10,6 +10,6 @@ func NewWorkerServerTarget() *Target {
 	return newTarget(TargetNameWorkerServer, &WorkerServerTargetOptions{})
 }
 
-func NewWorkerServerTargetResult() *TargetResult {
-	return newTargetResult(TargetNameWorkerServer, nil)
+func NewWorkerServerTargetResult(artifact *OsbuildArtifact) *TargetResult {
+	return newTargetResult(TargetNameWorkerServer, nil, artifact)
 }
diff --git a/internal/upload/koji/koji.go b/internal/upload/koji/koji.go
index b417cb8a4..bd9a7b00e 100644
--- a/internal/upload/koji/koji.go
+++ b/internal/upload/koji/koji.go
@@ -27,6 +27,7 @@ import (
 	"github.com/ubccr/kerby/khttp"
 
 	"github.com/osbuild/images/pkg/rpmmd"
+	"github.com/osbuild/osbuild-composer/internal/target"
 )
 
 type Koji struct {
@@ -37,10 +38,10 @@ type Koji struct {
 
 // BUILD METADATA
 
-// TypeInfo is a map whose entries are the names of the build types
+// TypeInfoBuild is a map whose entries are the names of the build types
 // used for the build, and the values are free-form maps containing
 // type-specific information for the build.
-type TypeInfo struct {
+type TypeInfoBuild struct {
 	// Image holds extra metadata about all images built by the build.
 	// It is a map whose keys are the filenames of the images, and
 	// the values are the extra metadata for the image.
@@ -51,7 +52,11 @@ type TypeInfo struct {
 // BuildExtra holds extra metadata associated with the build.
 // It is a free-form map, but must contain at least the 'typeinfo' key.
 type BuildExtra struct {
-	TypeInfo TypeInfo `json:"typeinfo"`
+	TypeInfo TypeInfoBuild `json:"typeinfo"`
+	// Manifest holds extra metadata about osbuild manifests attached to the build.
+	// It is a map whose keys are the filenames of the manifests, and
+	// the values are the extra metadata for the manifest.
+	Manifest map[string]*ManifestExtraInfo `json:"osbuild_manifest,omitempty"`
 }
 
 // Build represents a Koji build and holds metadata about it.
@@ -107,6 +112,10 @@ type BuildRoot struct {
 
 // OUTPUT METADATA
 
+type ImageOutputTypeExtraInfo interface {
+	isImageOutputTypeMD()
+}
+
 // ImageExtraInfo holds extra metadata about the image.
 // This structure is shared for the Extra metadata of the output and the build.
 type ImageExtraInfo struct {
@@ -116,18 +125,57 @@ type ImageExtraInfo struct {
 	Arch string `json:"arch"`
 	// Boot mode of the image
 	BootMode string `json:"boot_mode,omitempty"`
+	// Configuration used to prouce this image using osbuild
+	OSBuildArtifact *target.OsbuildArtifact `json:"osbuild_artifact,omitempty"`
+	// Version of the osbuild binary used by the worker to build the image
+	OSBuildVersion string `json:"osbuild_version,omitempty"`
+	// Results from any upload targets associated with the image
+	// except for the Koji target.
+	UploadTargetResults []*target.TargetResult `json:"upload_target_results,omitempty"`
+}
+
+func (ImageExtraInfo) isImageOutputTypeMD() {}
+
+type OSBuildComposerDepModule struct {
+	Path    string                    `json:"path"`
+	Version string                    `json:"version"`
+	Replace *OSBuildComposerDepModule `json:"replace,omitempty"`
 }
 
+// ManifestInfo holds information about the environment in which
+// the manifest was produced and which could affect its content.
+type ManifestInfo struct {
+	OSBuildComposerVersion string `json:"osbuild_composer_version"`
+	// List of relevant modules used by osbuild-composer which
+	// could affect the manifest content.
+	OSBuildComposerDeps []*OSBuildComposerDepModule `json:"osbuild_composer_deps,omitempty"`
+}
+
+// ManifestExtraInfo holds extra metadata about the osbuild manifest.
+type ManifestExtraInfo struct {
+	Arch string        `json:"arch"`
+	Info *ManifestInfo `json:"info,omitempty"`
+}
+
+func (ManifestExtraInfo) isImageOutputTypeMD() {}
+
 // BuildOutputExtra holds extra metadata associated with the build output.
 type BuildOutputExtra struct {
-	Image ImageExtraInfo `json:"image"`
+	// ImageOutput holds extra metadata about a single "image" output.
+	// "image" in this context is the "build type" in the Koji terminology,
+	// not necessarily an actual image. It can and must be used also for
+	// other supplementary files related to the image, such as osbuild manifest.
+	// The only exception are logs, which do not need to specify any "typeinfo".
+	ImageOutput ImageOutputTypeExtraInfo `json:"image"`
 }
 
 // BuildOutputType represents the type of a BuildOutput.
 type BuildOutputType string
 
 const (
-	BuildOutputTypeImage BuildOutputType = "image"
+	BuildOutputTypeImage    BuildOutputType = "image"
+	BuildOutputTypeLog      BuildOutputType = "log"
+	BuildOutputTypeManifest BuildOutputType = "osbuild-manifest"
 )
 
 // ChecksumType represents the type of a checksum used for a BuildOutput.
@@ -143,15 +191,15 @@ const (
 // The output can be a file of various types, which is imported to Koji.
 // Examples of types are "image", "log" or other.
 type BuildOutput struct {
-	BuildRootID  uint64           `json:"buildroot_id"`
-	Filename     string           `json:"filename"`
-	FileSize     uint64           `json:"filesize"`
-	Arch         string           `json:"arch"` // can be 'noarch' or a specific arch
-	ChecksumType ChecksumType     `json:"checksum_type"`
-	Checksum     string           `json:"checksum"`
-	Type         BuildOutputType  `json:"type"`
-	RPMs         []rpmmd.RPM      `json:"components"` // TODO: should be omitempty
-	Extra        BuildOutputExtra `json:"extra"`      // TODO: should be omitempty
+	BuildRootID  uint64            `json:"buildroot_id"`
+	Filename     string            `json:"filename"`
+	FileSize     uint64            `json:"filesize"`
+	Arch         string            `json:"arch"` // can be 'noarch' or a specific arch
+	ChecksumType ChecksumType      `json:"checksum_type"`
+	Checksum     string            `json:"checksum"`
+	Type         BuildOutputType   `json:"type"`
+	RPMs         []rpmmd.RPM       `json:"components,omitempty"`
+	Extra        *BuildOutputExtra `json:"extra,omitempty"`
 }
 
 // CONTENT GENERATOR METADATA
diff --git a/internal/worker/json.go b/internal/worker/json.go
index ae8305f52..67642fa70 100644
--- a/internal/worker/json.go
+++ b/internal/worker/json.go
@@ -3,6 +3,7 @@ package worker
 import (
 	"encoding/json"
 	"fmt"
+	"runtime/debug"
 
 	"github.com/osbuild/images/pkg/distro"
 	"github.com/osbuild/images/pkg/manifest"
@@ -10,6 +11,7 @@ import (
 	"github.com/osbuild/images/pkg/rpmmd"
 	"github.com/osbuild/osbuild-composer/internal/target"
 	"github.com/osbuild/osbuild-composer/internal/worker/clienterrors"
+	"golang.org/x/exp/slices"
 )
 
 //
@@ -61,6 +63,8 @@ type OSBuildJobResult struct {
 	// Boot mode supported by the image
 	// (string representation of distro.BootMode values)
 	ImageBootMode string `json:"image_boot_mode,omitempty"`
+	// Version of the osbuild binary used by the worker to build the image
+	OSBuildVersion string `json:"osbuild_version,omitempty"`
 	JobResult
 }
 
@@ -97,6 +101,19 @@ func (j *OSBuildJobResult) TargetResultsByName(name target.TargetName) []*target
 	return targetResults
 }
 
+// TargetResultsFilterByName iterates over TargetResults attached to the Job result and
+// returns a slice of Target results excluding the provided names (types). If there were
+// no TargetResults left after filtering, the returned slice will be empty.
+func (j *OSBuildJobResult) TargetResultsFilterByName(excludeNames []target.TargetName) []*target.TargetResult {
+	targetResults := []*target.TargetResult{}
+	for _, targetResult := range j.TargetResults {
+		if !slices.Contains(excludeNames, targetResult.Name) {
+			targetResults = append(targetResults, targetResult)
+		}
+	}
+	return targetResults
+}
+
 func (j *FileResolveJobResult) ResolutionErrors() []*clienterrors.Error {
 	resolutionErrors := []*clienterrors.Error{}
 
@@ -124,10 +141,11 @@ type KojiInitJobResult struct {
 }
 
 type KojiFinalizeJob struct {
-	Server        string   `json:"server"`
-	Name          string   `json:"name"`
-	Version       string   `json:"version"`
-	Release       string   `json:"release"`
+	Server  string `json:"server"`
+	Name    string `json:"name"`
+	Version string `json:"version"`
+	Release string `json:"release"`
+	// TODO: eventually deprecate and remove KojiFilenames, since the image filenames are now set in the KojiTargetResultOptions.
 	KojiFilenames []string `json:"koji_filenames"`
 	KojiDirectory string   `json:"koji_directory"`
 	TaskID        uint64   `json:"task_id"` /* https://pagure.io/koji/issue/215 */
@@ -250,9 +268,46 @@ type DepsolveJobResult struct {
 
 type ManifestJobByID struct{}
 
+// OSBuildComposerDepModule contains information about a module used by
+// osbuild-composer which could affect the manifest content.
+type OSBuildComposerDepModule struct {
+	Path    string                    `json:"path"`
+	Version string                    `json:"version"`
+	Replace *OSBuildComposerDepModule `json:"replace,omitempty"`
+}
+
+// ComposerDepModuleFromDebugModule converts a debug.Module instance
+// to an OSBuildComposerDepModule instance.
+func ComposerDepModuleFromDebugModule(module *debug.Module) *OSBuildComposerDepModule {
+	if module == nil {
+		return nil
+	}
+	depModule := &OSBuildComposerDepModule{
+		Path:    module.Path,
+		Version: module.Version,
+	}
+	if module.Replace != nil {
+		depModule.Replace = &OSBuildComposerDepModule{
+			Path:    module.Replace.Path,
+			Version: module.Replace.Version,
+		}
+	}
+	return depModule
+}
+
+// ManifestInfo contains information about the environment in which
+// the manifest was produced and which could affect its content.
+type ManifestInfo struct {
+	OSBuildComposerVersion string `json:"osbuild_composer_version"`
+	// List of relevant modules used by osbuild-composer which
+	// could affect the manifest content.
+	OSBuildComposerDeps []*OSBuildComposerDepModule `json:"osbuild_composer_deps,omitempty"`
+}
+
 type ManifestJobByIDResult struct {
-	Manifest manifest.OSBuildManifest `json:"data,omitempty"`
-	Error    string                   `json:"error"`
+	Manifest     manifest.OSBuildManifest `json:"data,omitempty"`
+	ManifestInfo ManifestInfo             `json:"info,omitempty"`
+	Error        string                   `json:"error"`
 	JobResult
 }
 
diff --git a/osbuild-composer.spec b/osbuild-composer.spec
index 5a412af1d..96be53b58 100644
--- a/osbuild-composer.spec
+++ b/osbuild-composer.spec
@@ -78,6 +78,11 @@ export PATH=$PWD/_bin${PATH:+:$PATH}
 export GOPATH=$GO_BUILD_PATH:%{gopath}
 export GOFLAGS+=" -mod=vendor"
 %endif
+%if 0%{?fedora}
+# Fedora disables Go modules by default, but we want to use them.
+# Undefine the macro which disables it to use the default behavior.
+%undefine gomodulesmode
+%endif
 
 # Set the commit hash so that composer can report what source version
 # was used to build it. This has to be set explicitly when calling rpmbuild,
diff --git a/vendor/github.com/aws/aws-sdk-go/aws/version.go b/vendor/github.com/aws/aws-sdk-go/aws/version.go
index f9673717c..e5d20a040 100644
--- a/vendor/github.com/aws/aws-sdk-go/aws/version.go
+++ b/vendor/github.com/aws/aws-sdk-go/aws/version.go
@@ -5,4 +5,4 @@ package aws
 const SDKName = "aws-sdk-go"
 
 // SDKVersion is the version of this SDK
-const SDKVersion = "1.45.16"
+const SDKVersion = "1.45.17"
diff --git a/vendor/github.com/aws/aws-sdk-go/service/ec2/api.go b/vendor/github.com/aws/aws-sdk-go/service/ec2/api.go
index bab95287a..5132ac3fc 100644
--- a/vendor/github.com/aws/aws-sdk-go/service/ec2/api.go
+++ b/vendor/github.com/aws/aws-sdk-go/service/ec2/api.go
@@ -52862,6 +52862,10 @@ func (c *EC2) RevokeSecurityGroupIngressRequest(input *RevokeSecurityGroupIngres
 // values, no error is returned, and the output describes the security group
 // rules that were not revoked.
 //
+// For a non-default VPC, if the values you specify do not match the existing
+// rule's values, an InvalidPermission.NotFound client error is returned, and
+// no rules are revoked.
+//
 // Amazon Web Services recommends that you describe the security group to verify
 // that the rules were removed.
 //
@@ -78184,6 +78188,10 @@ type CreateVerifiedAccessInstanceInput struct {
 	// it is UnauthorizedOperation.
 	DryRun *bool `type:"boolean"`
 
+	// Choose to enable or disable support for Federal Information Processing Standards
+	// (FIPS) on the instance.
+	FIPSEnabled *bool `type:"boolean"`
+
 	// The tags to assign to the Verified Access instance.
 	TagSpecifications []*TagSpecification `locationName:"TagSpecification" locationNameList:"item" type:"list"`
 }
@@ -78224,6 +78232,12 @@ func (s *CreateVerifiedAccessInstanceInput) SetDryRun(v bool) *CreateVerifiedAcc
 	return s
 }
 
+// SetFIPSEnabled sets the FIPSEnabled field's value.
+func (s *CreateVerifiedAccessInstanceInput) SetFIPSEnabled(v bool) *CreateVerifiedAccessInstanceInput {
+	s.FIPSEnabled = &v
+	return s
+}
+
 // SetTagSpecifications sets the TagSpecifications field's value.
 func (s *CreateVerifiedAccessInstanceInput) SetTagSpecifications(v []*TagSpecification) *CreateVerifiedAccessInstanceInput {
 	s.TagSpecifications = v
@@ -179083,6 +179097,10 @@ type VerifiedAccessInstance struct {
 	// A description for the Amazon Web Services Verified Access instance.
 	Description *string `locationName:"description" type:"string"`
 
+	// Describes if support for Federal Information Processing Standards (FIPS)
+	// is enabled on the instance.
+	FipsEnabled *bool `locationName:"fipsEnabled" type:"boolean"`
+
 	// The last updated time.
 	LastUpdatedTime *string `locationName:"lastUpdatedTime" type:"string"`
 
@@ -179126,6 +179144,12 @@ func (s *VerifiedAccessInstance) SetDescription(v string) *VerifiedAccessInstanc
 	return s
 }
 
+// SetFipsEnabled sets the FipsEnabled field's value.
+func (s *VerifiedAccessInstance) SetFipsEnabled(v bool) *VerifiedAccessInstance {
+	s.FipsEnabled = &v
+	return s
+}
+
 // SetLastUpdatedTime sets the LastUpdatedTime field's value.
 func (s *VerifiedAccessInstance) SetLastUpdatedTime(v string) *VerifiedAccessInstance {
 	s.LastUpdatedTime = &v
diff --git a/vendor/github.com/aws/aws-sdk-go/service/s3/api.go b/vendor/github.com/aws/aws-sdk-go/service/s3/api.go
index 188e37fdc..cf17e8299 100644
--- a/vendor/github.com/aws/aws-sdk-go/service/s3/api.go
+++ b/vendor/github.com/aws/aws-sdk-go/service/s3/api.go
@@ -42379,6 +42379,9 @@ const (
 
 	// ReplicationStatusReplica is a ReplicationStatus enum value
 	ReplicationStatusReplica = "REPLICA"
+
+	// ReplicationStatusCompleted is a ReplicationStatus enum value
+	ReplicationStatusCompleted = "COMPLETED"
 )
 
 // ReplicationStatus_Values returns all elements of the ReplicationStatus enum
@@ -42388,6 +42391,7 @@ func ReplicationStatus_Values() []string {
 		ReplicationStatusPending,
 		ReplicationStatusFailed,
 		ReplicationStatusReplica,
+		ReplicationStatusCompleted,
 	}
 }
 
diff --git a/vendor/modules.txt b/vendor/modules.txt
index 0fd5a4271..330bc76db 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -119,7 +119,7 @@ github.com/acarl005/stripansi
 # github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2
 ## explicit; go 1.13
 github.com/asaskevich/govalidator
-# github.com/aws/aws-sdk-go v1.45.16
+# github.com/aws/aws-sdk-go v1.45.17
 ## explicit; go 1.11
 github.com/aws/aws-sdk-go/aws
 github.com/aws/aws-sdk-go/aws/arn


Test patch:
diff --git a/cmd/osbuild-koji-tests/main_test.go b/cmd/osbuild-koji-tests/main_test.go
index 0045037ad..16012b467 100644
--- a/cmd/osbuild-koji-tests/main_test.go
+++ b/cmd/osbuild-koji-tests/main_test.go
@@ -179,8 +179,8 @@ func TestKojiImport(t *testing.T) {
 			Checksum:     hash,
 			Type:         koji.BuildOutputTypeImage,
 			RPMs:         []rpmmd.RPM{},
-			Extra: koji.BuildOutputExtra{
-				Image: koji.ImageExtraInfo{
+			Extra: &koji.BuildOutputExtra{
+				ImageOutput: koji.ImageExtraInfo{
 					Arch:     "noarch",
 					BootMode: distro.BOOT_LEGACY.String(),
 				},
diff --git a/internal/cloudapi/v2/v2_koji_test.go b/internal/cloudapi/v2/v2_koji_test.go
index 69d5ce31c..9d1611277 100644
--- a/internal/cloudapi/v2/v2_koji_test.go
+++ b/internal/cloudapi/v2/v2_koji_test.go
@@ -50,8 +50,15 @@ func TestKojiCompose(t *testing.T) {
 				Arch:   test_distro.TestArchName,
 				HostOS: test_distro.TestDistroName,
 				TargetResults: []*target.TargetResult{target.NewKojiTargetResult(&target.KojiTargetResultOptions{
-					ImageMD5:  "browns",
-					ImageSize: 42,
+					Image: &target.KojiOutputInfo{
+						Filename:     "test.img",
+						ChecksumType: target.ChecksumTypeMD5,
+						Checksum:     "browns",
+						Size:         42,
+					},
+				}, &target.OsbuildArtifact{
+					ExportFilename: "disk.img",
+					ExportName:     "image",
 				})},
 				OSBuildOutput: &osbuild.Result{
 					Success: true,
@@ -87,8 +94,15 @@ func TestKojiCompose(t *testing.T) {
 				Arch:   test_distro.TestArchName,
 				HostOS: test_distro.TestDistroName,
 				TargetResults: []*target.TargetResult{target.NewKojiTargetResult(&target.KojiTargetResultOptions{
-					ImageMD5:  "browns",
-					ImageSize: 42,
+					Image: &target.KojiOutputInfo{
+						Filename:     "test.img",
+						ChecksumType: target.ChecksumTypeMD5,
+						Checksum:     "browns",
+						Size:         42,
+					},
+				}, &target.OsbuildArtifact{
+					ExportFilename: "disk.img",
+					ExportName:     "image",
 				})},
 				OSBuildOutput: &osbuild.Result{
 					Success: true,
@@ -124,8 +138,15 @@ func TestKojiCompose(t *testing.T) {
 				Arch:   test_distro.TestArchName,
 				HostOS: test_distro.TestDistroName,
 				TargetResults: []*target.TargetResult{target.NewKojiTargetResult(&target.KojiTargetResultOptions{
-					ImageMD5:  "browns",
-					ImageSize: 42,
+					Image: &target.KojiOutputInfo{
+						Filename:     "test.img",
+						ChecksumType: target.ChecksumTypeMD5,
+						Checksum:     "browns",
+						Size:         42,
+					},
+				}, &target.OsbuildArtifact{
+					ExportFilename: "disk.img",
+					ExportName:     "image",
 				})},
 				OSBuildOutput: &osbuild.Result{
 					Success: true,
@@ -160,8 +181,15 @@ func TestKojiCompose(t *testing.T) {
 				Arch:   test_distro.TestArchName,
 				HostOS: test_distro.TestDistroName,
 				TargetResults: []*target.TargetResult{target.NewKojiTargetResult(&target.KojiTargetResultOptions{
-					ImageMD5:  "browns",
-					ImageSize: 42,
+					Image: &target.KojiOutputInfo{
+						Filename:     "test.img",
+						ChecksumType: target.ChecksumTypeMD5,
+						Checksum:     "browns",
+						Size:         42,
+					},
+				}, &target.OsbuildArtifact{
+					ExportFilename: "disk.img",
+					ExportName:     "image",
 				})},
 				OSBuildOutput: &osbuild.Result{
 					Success: false,
@@ -198,8 +226,15 @@ func TestKojiCompose(t *testing.T) {
 				Arch:   test_distro.TestArchName,
 				HostOS: test_distro.TestDistroName,
 				TargetResults: []*target.TargetResult{target.NewKojiTargetResult(&target.KojiTargetResultOptions{
-					ImageMD5:  "browns",
-					ImageSize: 42,
+					Image: &target.KojiOutputInfo{
+						Filename:     "test.img",
+						ChecksumType: target.ChecksumTypeMD5,
+						Checksum:     "browns",
+						Size:         42,
+					},
+				}, &target.OsbuildArtifact{
+					ExportFilename: "disk.img",
+					ExportName:     "image",
 				})},
 				OSBuildOutput: &osbuild.Result{
 					Success: true,
@@ -247,8 +282,15 @@ func TestKojiCompose(t *testing.T) {
 				Arch:   test_distro.TestArchName,
 				HostOS: test_distro.TestDistroName,
 				TargetResults: []*target.TargetResult{target.NewKojiTargetResult(&target.KojiTargetResultOptions{
-					ImageMD5:  "browns",
-					ImageSize: 42,
+					Image: &target.KojiOutputInfo{
+						Filename:     "test.img",
+						ChecksumType: target.ChecksumTypeMD5,
+						Checksum:     "browns",
+						Size:         42,
+					},
+				}, &target.OsbuildArtifact{
+					ExportFilename: "disk.img",
+					ExportName:     "image",
 				})},
 				OSBuildOutput: &osbuild.Result{
 					Success: true,
@@ -288,8 +330,15 @@ func TestKojiCompose(t *testing.T) {
 				Arch:   test_distro.TestArchName,
 				HostOS: test_distro.TestDistroName,
 				TargetResults: []*target.TargetResult{target.NewKojiTargetResult(&target.KojiTargetResultOptions{
-					ImageMD5:  "browns",
-					ImageSize: 42,
+					Image: &target.KojiOutputInfo{
+						Filename:     "test.img",
+						ChecksumType: target.ChecksumTypeMD5,
+						Checksum:     "browns",
+						Size:         42,
+					},
+				}, &target.OsbuildArtifact{
+					ExportFilename: "disk.img",
+					ExportName:     "image",
 				})},
 				OSBuildOutput: &osbuild.Result{
 					Success: true,
diff --git a/internal/cloudapi/v2/v2_test.go b/internal/cloudapi/v2/v2_test.go
index 149232f4d..fc5d42496 100644
--- a/internal/cloudapi/v2/v2_test.go
+++ b/internal/cloudapi/v2/v2_test.go
@@ -1261,6 +1261,9 @@ func TestImageFromCompose(t *testing.T) {
 	tr := target.NewAWSTargetResult(&target.AWSTargetResultOptions{
 		Ami:    "ami-abc123",
 		Region: "eu-central-1",
+	}, &target.OsbuildArtifact{
+		ExportFilename: "image.raw",
+		ExportName:     "image",
 	})
 	res, err := json.Marshal(&worker.OSBuildJobResult{
 		Success:       true,
diff --git a/internal/target/koji_target_test.go b/internal/target/koji_target_test.go
new file mode 100644
index 000000000..41b03e025
--- /dev/null
+++ b/internal/target/koji_target_test.go
@@ -0,0 +1,163 @@
+package target
+
+import (
+	"encoding/json"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+// TestKojiTargetOptionsUnmarshalJSON tests that the resulting struct
+// has appropriate fields set when legacy JSON is used.
+func TestKojiTargetResultOptionsUnmarshalJSON(t *testing.T) {
+	type testCase struct {
+		name           string
+		JSON           []byte
+		expectedResult *KojiTargetResultOptions
+		err            bool
+	}
+
+	testCases := []testCase{
+		{
+			name: "new format",
+			JSON: []byte(`{"image":{"checksum_type":"md5","checksum":"hash","filename":"image.raw","size":123456}}`),
+			expectedResult: &KojiTargetResultOptions{
+				Image: &KojiOutputInfo{
+					Filename:     "image.raw",
+					ChecksumType: "md5",
+					Checksum:     "hash",
+					Size:         123456,
+				},
+			},
+		},
+		{
+			name: "old format",
+			JSON: []byte(`{"image_md5":"hash","image_size":123456}`),
+			expectedResult: &KojiTargetResultOptions{
+				Image: &KojiOutputInfo{
+					ChecksumType: "md5",
+					Checksum:     "hash",
+					Size:         123456,
+				},
+			},
+		},
+		{
+			name: "full format",
+			JSON: []byte(`{"image":{"checksum_type":"md5","checksum":"hash","filename":"image.raw","size":123456},"log":{"checksum_type":"md5","checksum":"hash","filename":"log.txt","size":123456},"osbuild_manifest":{"checksum_type":"md5","checksum":"hash","filename":"manifest.json","size":123456}}`),
+			expectedResult: &KojiTargetResultOptions{
+				Image: &KojiOutputInfo{
+					Filename:     "image.raw",
+					ChecksumType: "md5",
+					Checksum:     "hash",
+					Size:         123456,
+				},
+				Log: &KojiOutputInfo{
+					Filename:     "log.txt",
+					ChecksumType: "md5",
+					Checksum:     "hash",
+					Size:         123456,
+				},
+				OSBuildManifest: &KojiOutputInfo{
+					Filename:     "manifest.json",
+					ChecksumType: "md5",
+					Checksum:     "hash",
+					Size:         123456,
+				},
+			},
+		},
+		{
+			name: "invalid JSON",
+			JSON: []byte(`{"image_md5":"hash","image_size":123456`),
+			err:  true,
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			var result KojiTargetResultOptions
+			err := json.Unmarshal(tc.JSON, &result)
+			if tc.err {
+				assert.Error(t, err)
+				return
+			} else {
+				assert.NoError(t, err)
+			}
+			assert.Equal(t, tc.expectedResult, &result)
+		})
+	}
+}
+
+// TestKojiTargetResultOptionsMarshalJSON tests that the resulting JSON
+// has the legacy fields set for backwards compatibility.
+func TestKojiTargetResultOptionsMarshalJSON(t *testing.T) {
+	type testCase struct {
+		name         string
+		results      *KojiTargetResultOptions
+		expectedJSON []byte
+		err          bool
+	}
+
+	testCases := []testCase{
+		{
+			name: "backwards compatibility",
+			results: &KojiTargetResultOptions{
+				Image: &KojiOutputInfo{
+					Filename:     "image.raw",
+					ChecksumType: ChecksumTypeMD5,
+					Checksum:     "hash",
+					Size:         123456,
+				},
+			},
+			expectedJSON: []byte(`{"image":{"filename":"image.raw","checksum_type":"md5","checksum":"hash","size":123456},"image_md5":"hash","image_size":123456}`),
+		},
+		{
+			name: "full format",
+			results: &KojiTargetResultOptions{
+				Image: &KojiOutputInfo{
+					Filename:     "image.raw",
+					ChecksumType: ChecksumTypeMD5,
+					Checksum:     "hash",
+					Size:         123456,
+				},
+				Log: &KojiOutputInfo{
+					Filename:     "log.txt",
+					ChecksumType: ChecksumTypeMD5,
+					Checksum:     "hash",
+					Size:         654321,
+				},
+				OSBuildManifest: &KojiOutputInfo{
+					Filename:     "manifest.json",
+					ChecksumType: ChecksumTypeMD5,
+					Checksum:     "hash",
+					Size:         123321,
+				},
+			},
+			expectedJSON: []byte(`{"image":{"filename":"image.raw","checksum_type":"md5","checksum":"hash","size":123456},"log":{"filename":"log.txt","checksum_type":"md5","checksum":"hash","size":654321},"osbuild_manifest":{"filename":"manifest.json","checksum_type":"md5","checksum":"hash","size":123321},"image_md5":"hash","image_size":123456}`),
+		},
+		{
+			name: "invalid checksum type",
+			results: &KojiTargetResultOptions{
+				Image: &KojiOutputInfo{
+					Filename:     "image.raw",
+					ChecksumType: "sha256",
+					Checksum:     "hash",
+					Size:         123456,
+				},
+			},
+			err: true,
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			result, err := json.Marshal(tc.results)
+			if tc.err {
+				assert.Error(t, err)
+				return
+			} else {
+				assert.NoError(t, err)
+			}
+			assert.Equal(t, tc.expectedJSON, result)
+		})
+	}
+}
diff --git a/internal/target/targetresult_test.go b/internal/target/targetresult_test.go
index dcc7a884f..b92bf9785 100644
--- a/internal/target/targetresult_test.go
+++ b/internal/target/targetresult_test.go
@@ -58,12 +58,16 @@ func TestTargetResultUnmarshal(t *testing.T) {
 			},
 		},
 		{
-			resultJSON: []byte(`{"name":"org.osbuild.koji","options":{"image_md5":"hash","image_size":123456}}`),
+			resultJSON: []byte(`{"name":"org.osbuild.koji","options":{"image":{"checksum_type":"md5","checksum":"hash","filename":"image.raw","size":123456}}}`),
 			expectedResult: &TargetResult{
 				Name: TargetNameKoji,
 				Options: &KojiTargetResultOptions{
-					ImageMD5:  "hash",
-					ImageSize: 123456,
+					Image: &KojiOutputInfo{
+						Filename:     "image.raw",
+						ChecksumType: ChecksumTypeMD5,
+						Checksum:     "hash",
+						Size:         123456,
+					},
 				},
 			},
 		},
diff --git a/internal/worker/json_test.go b/internal/worker/json_test.go
index 310e1839f..b208516dd 100644
--- a/internal/worker/json_test.go
+++ b/internal/worker/json_test.go
@@ -189,6 +189,150 @@ func TestOSBuildJobResultTargetResultsByName(t *testing.T) {
 	}
 }
 
+func TestOSBuildJobResultTargetResultsFilterByName(t *testing.T) {
+	testCases := []struct {
+		jobResult     OSBuildJobResult
+		targetNames   []target.TargetName
+		targetResults []*target.TargetResult
+	}{
+		{
+			jobResult: OSBuildJobResult{
+				TargetResults: []*target.TargetResult{
+					{
+						Name:        target.TargetNameAWS,
+						TargetError: clienterrors.WorkerClientError(clienterrors.ErrorInvalidTargetConfig, "can't login to AWS", nil),
+					},
+					{
+						Name:        target.TargetNameVMWare,
+						TargetError: clienterrors.WorkerClientError(clienterrors.ErrorUploadingImage, "can't upload image to VMWare", nil),
+					},
+					{
+						Name:        target.TargetNameVMWare,
+						TargetError: clienterrors.WorkerClientError(clienterrors.ErrorUploadingImage, "can't upload image to VMWare", nil),
+					},
+					{
+						Name:        target.TargetNameAWSS3,
+						TargetError: clienterrors.WorkerClientError(clienterrors.ErrorUploadingImage, "failed to upload image to AWS S3", nil),
+					},
+				},
+			},
+			targetNames: []target.TargetName{
+				target.TargetNameVMWare,
+			},
+			targetResults: []*target.TargetResult{
+				{
+					Name:        target.TargetNameAWS,
+					TargetError: clienterrors.WorkerClientError(clienterrors.ErrorInvalidTargetConfig, "can't login to AWS", nil),
+				},
+				{
+					Name:        target.TargetNameAWSS3,
+					TargetError: clienterrors.WorkerClientError(clienterrors.ErrorUploadingImage, "failed to upload image to AWS S3", nil),
+				},
+			},
+		},
+		{
+			jobResult: OSBuildJobResult{
+				TargetResults: []*target.TargetResult{
+					{
+						Name:        target.TargetNameAWS,
+						TargetError: clienterrors.WorkerClientError(clienterrors.ErrorInvalidTargetConfig, "can't login to AWS", nil),
+					},
+					{
+						Name:        target.TargetNameVMWare,
+						TargetError: clienterrors.WorkerClientError(clienterrors.ErrorUploadingImage, "can't upload image to VMWare", nil),
+					},
+					{
+						Name:        target.TargetNameVMWare,
+						TargetError: clienterrors.WorkerClientError(clienterrors.ErrorUploadingImage, "can't upload image to VMWare", nil),
+					},
+					{
+						Name:        target.TargetNameAWSS3,
+						TargetError: clienterrors.WorkerClientError(clienterrors.ErrorUploadingImage, "failed to upload image to AWS S3", nil),
+					},
+				},
+			},
+			targetNames: []target.TargetName{
+				target.TargetNameVMWare,
+				target.TargetNameAWSS3,
+			},
+			targetResults: []*target.TargetResult{
+				{
+					Name:        target.TargetNameAWS,
+					TargetError: clienterrors.WorkerClientError(clienterrors.ErrorInvalidTargetConfig, "can't login to AWS", nil),
+				},
+			},
+		},
+		{
+			jobResult: OSBuildJobResult{
+				TargetResults: []*target.TargetResult{
+					{
+						Name:        target.TargetNameAWS,
+						TargetError: clienterrors.WorkerClientError(clienterrors.ErrorInvalidTargetConfig, "can't login to AWS", nil),
+					},
+					{
+						Name:        target.TargetNameVMWare,
+						TargetError: clienterrors.WorkerClientError(clienterrors.ErrorUploadingImage, "can't upload image to VMWare", nil),
+					},
+					{
+						Name:        target.TargetNameVMWare,
+						TargetError: clienterrors.WorkerClientError(clienterrors.ErrorUploadingImage, "can't upload image to VMWare", nil),
+					},
+					{
+						Name:        target.TargetNameAWSS3,
+						TargetError: clienterrors.WorkerClientError(clienterrors.ErrorUploadingImage, "failed to upload image to AWS S3", nil),
+					},
+				},
+			},
+			targetNames: []target.TargetName{
+				target.TargetNameAWS,
+				target.TargetNameAWSS3,
+			},
+			targetResults: []*target.TargetResult{
+				{
+					Name:        target.TargetNameVMWare,
+					TargetError: clienterrors.WorkerClientError(clienterrors.ErrorUploadingImage, "can't upload image to VMWare", nil),
+				},
+				{
+					Name:        target.TargetNameVMWare,
+					TargetError: clienterrors.WorkerClientError(clienterrors.ErrorUploadingImage, "can't upload image to VMWare", nil),
+				},
+			},
+		},
+		{
+			jobResult: OSBuildJobResult{
+				TargetResults: []*target.TargetResult{
+					{
+						Name:        target.TargetNameAWS,
+						TargetError: clienterrors.WorkerClientError(clienterrors.ErrorInvalidTargetConfig, "can't login to AWS", nil),
+					},
+					{
+						Name:        target.TargetNameVMWare,
+						TargetError: clienterrors.WorkerClientError(clienterrors.ErrorUploadingImage, "can't upload image to VMWare", nil),
+					},
+					{
+						Name:        target.TargetNameVMWare,
+						TargetError: clienterrors.WorkerClientError(clienterrors.ErrorUploadingImage, "can't upload image to VMWare", nil),
+					},
+					{
+						Name:        target.TargetNameAWSS3,
+						TargetError: clienterrors.WorkerClientError(clienterrors.ErrorUploadingImage, "failed to upload image to AWS S3", nil),
+					},
+				},
+			},
+			targetNames: []target.TargetName{
+				target.TargetNameAWS,
+				target.TargetNameVMWare,
+				target.TargetNameAWSS3,
+			},
+			targetResults: []*target.TargetResult{},
+		},
+	}
+
+	for _, testCase := range testCases {
+		assert.EqualValues(t, testCase.targetResults, testCase.jobResult.TargetResultsFilterByName(testCase.targetNames))
+	}
+}
+
 func TestOSBuildJobExports(t *testing.T) {
 	testCases := []struct {
 		job             *OSBuildJob
diff --git a/test/cases/koji.sh b/test/cases/koji.sh
index f4f3c3f4a..7cb04d209 100755
--- a/test/cases/koji.sh
+++ b/test/cases/koji.sh
@@ -35,6 +35,9 @@ TEST_TYPE_CLOUD_UPLOAD="cloud-upload"
 # test Koji compose via cloudapi without upload to cloud by default
 TEST_TYPE="${1:-$TEST_TYPE_CLOUDAPI}"
 
+# Koji hub URL to use for testing
+KOJI_HUB_URL="http://localhost:8080/kojihub"
+
 #
 # Cloud upload - check environment and prepare it
 #
@@ -114,12 +117,15 @@ trap cleanups EXIT
 
 # Verify that all the expected information is present in the buildinfo
 function verify_buildinfo() {
-    local buildinfo="${1}"
+    local buildid="${1}"
     local target_cloud="${2:-none}"
 
+    local osbuild_version
+    osbuild_version="$(osbuild --version | cut -d ' ' -f 2 -)"
+
     local extra_build_metadata
     # extract the extra build metadata JSON from the output
-    extra_build_metadata="$(echo "${buildinfo}" | grep -oP '(?<=Extra: ).*' | tr "'" '"')"
+    extra_build_metadata="$(koji -s "${KOJI_HUB_URL}" --noauth call --json getBuild "${buildid}" | jq -r '.extra')"
 
     # sanity check the extra build metadata
     if [ -z "${extra_build_metadata}" ]; then
@@ -129,53 +135,192 @@ function verify_buildinfo() {
 
     # extract the image archives paths from the output and keep only the filenames
     local outputs_images
-    outputs_images="$(echo "${buildinfo}" |
-        sed -zE 's/.*Image archives:\n((\S+\n){1,})([\w\s]+:){0,}.*/\1/g' |
-        sed -E 's/.*\/(.*)/\1/g')"
+    outputs_images="$(koji -s "${KOJI_HUB_URL}" --noauth call --json listArchives "${buildid}" | jq -r 'map(select(.btype == "image" and .type_name != "json"))')"
 
     # we build one image for cloud test case and two for non-cloud test case
+    local outputs_images_count
+    outputs_images_count="$(echo "${outputs_images}" | jq 'length')"
     if [ "${target_cloud}" == "none" ]; then
-        if [[ $(echo "${outputs_images}" | wc -l) -ne 2 ]]; then
-            echo "Unexpected number of images in the buildinfo"
+        if [ "${outputs_images_count}" -ne 2 ]; then
+            echo "Unexpected number of images in the buildinfo. Want 2, got ${outputs_images_count}."
             exit 1
         fi
     else
-        if [[ $(echo "${outputs_images}" | wc -l) -ne 1 ]]; then
-            echo "Unexpected number of images in the buildinfo"
+        if [ "${outputs_images_count}" -ne 1 ]; then
+            echo "Unexpected number of images in the buildinfo. Want 1, got ${outputs_images_count}."
+            exit 1
+        fi
+
+        # Verify that the target results are present in the image output metadata
+        local target_results
+        target_results="$(echo "${outputs_images}" | jq -r '.[0].extra.image.upload_target_results')"
+        local target_results_count
+        target_results_count="$(echo "${target_results}" | jq 'length')"
+        if [ "$target_results_count" -ne 1 ]; then
+            echo "Unexpected number of target results in the buildinfo. Want 1, got ${target_results_count}."
             exit 1
         fi
+
+        local target_result_name
+        target_result_name="$(echo "${target_results}" | jq -r '.[0].name')"
+        local want_target_result_name
+        case ${target_cloud} in
+            "$CLOUD_PROVIDER_AWS")
+                want_target_result_name="org.osbuild.aws"
+                ;;
+            "$CLOUD_PROVIDER_GCP")
+                want_target_result_name="org.osbuild.gcp"
+                ;;
+            "$CLOUD_PROVIDER_AZURE")
+                want_target_result_name="org.osbuild.azure.image"
+                ;;
+            *)
+                echo "Unknown cloud provider: ${CLOUD_PROVIDER}"
+                exit 1
+        esac
+        if [ "${target_result_name}" != "${want_target_result_name}" ]; then
+            echo "Unexpected target result in the buildinfo. Want '${want_target_result_name}', got '${target_result_name}'."
+            exit 1
+        fi
+    fi
+
+    local outputs_manifests
+    outputs_manifests="$(koji -s "${KOJI_HUB_URL}" --noauth call --json listArchives "${buildid}" | jq -r 'map(select(.btype == "image" and .type_name == "json"))')"
+    local outputs_manifests_count
+    outputs_manifests_count="$(echo "${outputs_manifests}" | jq 'length')"
+    if [ "${outputs_manifests_count}" -ne "${outputs_images_count}" ]; then
+        echo "Mismatch between the number of image archives and image manifests in the buildinfo"
+        exit 1
+    fi
+
+    local outputs_logs
+    outputs_logs="$(koji -s "${KOJI_HUB_URL}" --noauth call --json getBuildLogs "${buildid}" | jq -r 'map(select(.name != "cg_import.log"))')"
+    local outputs_logs_count
+    outputs_logs_count="$(echo "${outputs_logs}" | jq 'length')"
+    if [ "${outputs_logs_count}" -ne "${outputs_images_count}" ]; then
+        echo "Mismatch between the number of image archives and image logs in the buildinfo"
+        exit 1
     fi
 
-    local images_metadata
-    images_metadata="$(echo "${extra_build_metadata}" | jq -r '.typeinfo.image')"
+    local build_extra_md_image
+    build_extra_md_image="$(echo "${extra_build_metadata}" | jq -r '.typeinfo.image')"
 
-    for image in $outputs_images; do
-        local image_metadata
-        image_metadata="$(echo "${images_metadata}" | jq -r ".\"${image}\"")"
-        if [ "${image_metadata}" == "null" ]; then
-            echo "Image metadata for '${image}' is missing"
+    for image_idx in $(seq 0 $((outputs_images_count - 1))); do
+        local image
+        image="$(echo "${outputs_images}" | jq -r ".[${image_idx}]")"
+
+        local image_filename
+        image_filename="$(echo "${image}" | jq -r '.filename')"
+
+        local image_metadata_build
+        image_metadata_build="$(echo "${build_extra_md_image}" | jq -r ".\"${image_filename}\"")"
+        if [ "${image_metadata_build}" == "null" ]; then
+            echo "Image metadata for '${image_filename}' is missing"
             exit 1
         fi
 
         local image_arch
-        image_arch="$(echo "${image_metadata}" | jq -r '.arch')"
+        image_arch="$(echo "${image_metadata_build}" | jq -r '.arch')"
         if [ "${image_arch}" != "${ARCH}" ]; then
-            echo "Unexpected arch for '${image}'. Expected '${ARCH}', but got '${image_arch}'"
+            echo "Unexpected arch for '${image_filename}'. Expected '${ARCH}', but got '${image_arch}'"
             exit 1
         fi
 
         local image_boot_mode
-        image_boot_mode="$(echo "${image_metadata}" | jq -r '.boot_mode')"
+        image_boot_mode="$(echo "${image_metadata_build}" | jq -r '.boot_mode')"
         # for now, check just that the boot mode is a valid value
         case "${image_boot_mode}" in
             "uefi"|"legacy"|"hybrid")
                 ;;
             "none"|*)
                 # for now, we don't upload any images that have 'none' as boot mode, although it is a valid value
-                echo "Unexpected boot mode for '${image}'. Expected 'uefi', 'legacy' or 'hybrid', but got '${image_boot_mode}'"
+                echo "Unexpected boot mode for '${image_filename}'. Expected 'uefi', 'legacy' or 'hybrid', but got '${image_boot_mode}'"
                 exit 1
                 ;;
         esac
+
+        local image_osbuild_artifact
+        image_osbuild_artifact="$(echo "${image_metadata_build}" | jq -r '.osbuild_artifact')"
+        if [ "${image_osbuild_artifact}" == "null" ]; then
+            echo "Image osbuild artifact information for '${image_filename}' is missing"
+            exit 1
+        fi
+
+        local image_osbuild_version
+        image_osbuild_version="$(echo "${image_metadata_build}" | jq -r '.osbuild_version')"
+        if [ "${image_osbuild_version}" != "${osbuild_version}" ]; then
+            echo "Unexpected osbuild version for '${image_filename}'. Expected '${osbuild_version}', but got '${image_osbuild_version}'"
+            exit 1
+        fi
+
+        local image_metadata_archive
+        image_metadata_archive="$(echo "${image}" | jq -r '.extra.image')"
+        if [ "${image_metadata_build}" != "${image_metadata_archive}" ]; then
+            echo "Image extra metadata for '${image_filename}' in the build metadata and in the archive metadata differ"
+            exit 1
+        fi
+    done
+
+    local build_extra_md_manifest
+    build_extra_md_manifest="$(echo "${extra_build_metadata}" | jq -r '.osbuild_manifest')"
+
+    for manifest_idx in $(seq 0 $((outputs_manifests_count - 1))); do
+        local manifest
+        manifest="$(echo "${outputs_manifests}" | jq -r ".[${manifest_idx}]")"
+
+        local manifest_filename
+        manifest_filename="$(echo "${manifest}" | jq -r '.filename')"
+
+        local manifest_metadata_build
+        manifest_metadata_build="$(echo "${build_extra_md_manifest}" | jq -r ".\"${manifest_filename}\"")"
+        if [ "${manifest_metadata_build}" == "null" ]; then
+            echo "Manifest metadata for '${manifest_filename}' is missing"
+            exit 1
+        fi
+
+        local manifest_arch
+        manifest_arch="$(echo "${manifest_metadata_build}" | jq -r '.arch')"
+        if [ "${image_arch}" != "${ARCH}" ]; then
+            echo "Unexpected arch for '${manifest_filename}'. Expected '${ARCH}', but got '${manifest_arch}'"
+            exit 1
+        fi
+
+        local manifest_info
+        manifest_info="$(echo "${manifest_metadata_build}" | jq -r '.info')"
+        if [ "${manifest_info}" == "null" ]; then
+            echo "Manifest info for '${manifest_filename}' is missing"
+            exit 1
+        fi
+
+        if [ "$(echo "${manifest_info}" | jq -r '.osbuild_composer_version')" == "null" ]; then
+            echo "Manifest info for '${manifest_filename}' is missing osbuild-composer version"
+            exit 1
+        fi
+
+        # check osbuild/images version info in the metadata
+        local osbuild_composer_deps
+        osbuild_composer_deps="$(echo "${manifest_info}" | jq -r '.osbuild_composer_deps')"
+        if [ "$(echo "${osbuild_composer_deps}" | jq 'length')" -ne 1 ]; then
+            echo "Manifest info for '${manifest_filename}' has unexpected number of osbuild-composer dependencies. \
+                Expected 1, got '$(echo "${osbuild_composer_deps}" | jq 'length')'"
+            exit 1
+        fi
+        if [ "$(echo "${osbuild_composer_deps}" | jq -r '.[0].path')" != "github.com/osbuild/images" ]; then
+            echo "Manifest info for '${manifest_filename}' has unexpected osbuild-composer dependency path. \
+                Expected 'github.com/osbuild/images', got '$(echo "${osbuild_composer_deps}" | jq -r '.[0].path')'"
+            exit 1
+        fi
+        if [ "$(echo "${osbuild_composer_deps}" | jq -r '.[0].version')" == "null" ]; then
+            echo "Manifest info for '${manifest_filename}' has missing 'github.com/osbuild/images' dependency version"
+            exit 1
+        fi
+
+        local manifest_metadata_archive
+        manifest_metadata_archive="$(echo "${manifest}" | jq -r '.extra.image')"
+        if [ "${manifest_metadata_build}" != "${manifest_metadata_archive}" ]; then
+            echo "Manifest extra metadata for '${manifest_filename}' in the build metadata and in the archive metadata differ"
+            exit 1
+        fi
     done
 }
 
@@ -200,10 +345,10 @@ sudo cp \
 sudo update-ca-trust
 
 greenprint "Testing Koji"
-koji --server=http://localhost:8080/kojihub --user=osbuild --password=osbuildpass --authtype=password hello
+koji --server="${KOJI_HUB_URL}" --user=osbuild --password=osbuildpass --authtype=password hello
 
 greenprint "Creating Koji task"
-koji --server=http://localhost:8080/kojihub --user kojiadmin --password kojipass --authtype=password make-task image
+koji --server="${KOJI_HUB_URL}" --user kojiadmin --password kojipass --authtype=password make-task image
 
 # Always build the latest RHEL - that suits the koji API usecase the most.
 if [[ "$DISTRO_CODE" == rhel-8* ]]; then
@@ -270,14 +415,22 @@ if [[ "$TEST_TYPE" == "$TEST_TYPE_CLOUD_UPLOAD" ]]; then
 fi
 
 greenprint "Show Koji task"
-koji --server=http://localhost:8080/kojihub taskinfo 1
+koji --server="${KOJI_HUB_URL}" taskinfo 1
 
 greenprint "Show Koji buildinfo"
-BUILDINFO_OUTPUT="$(koji --server=http://localhost:8080/kojihub buildinfo 1)"
-echo "${BUILDINFO_OUTPUT}"
+koji --server="${KOJI_HUB_URL}" buildinfo 1
+
+greenprint "Show Koji raw buildinfo"
+koji --server="${KOJI_HUB_URL}" --noauth call --json getBuild 1
+
+greenprint "Show Koji build archives"
+koji --server="${KOJI_HUB_URL}" --noauth call --json listArchives 1
+
+greenprint "Show Koji build logs"
+koji --server="${KOJI_HUB_URL}" --noauth call --json getBuildLogs 1
 
-greenprint "Verify the buildinfo output"
-verify_buildinfo "${BUILDINFO_OUTPUT}" "${CLOUD_PROVIDER}"
+greenprint "Verify the Koji build info and metadata"
+verify_buildinfo 1 "${CLOUD_PROVIDER}"
 
 greenprint "Run the integration test"
 sudo /usr/libexec/osbuild-composer-test/osbuild-koji-tests