diff --git a/cmd/flux/create_source_oci.go b/cmd/flux/create_source_oci.go
index e1d26c4351e719ec30a457fbb3a6079dd9ba624e..a97c64fb12d3c82017108ced35f54cb727ccecbf 100644
--- a/cmd/flux/create_source_oci.go
+++ b/cmd/flux/create_source_oci.go
@@ -19,7 +19,6 @@ package main
 import (
 	"context"
 	"fmt"
-	"os"
 	"strings"
 
 	"github.com/spf13/cobra"
@@ -43,7 +42,7 @@ var createSourceOCIRepositoryCmd = &cobra.Command{
 	Long:  `The create source oci command generates an OCIRepository resource and waits for it to be ready.`,
 	Example: `  # Create an OCIRepository for a public container image
   flux create source oci podinfo \
-	--url=ghcr.io/stefanprodan/manifests/podinfo \
+    --url=ghcr.io/stefanprodan/manifests/podinfo \
     --tag=6.1.6 \
     --interval=10m
 `,
@@ -51,11 +50,13 @@ var createSourceOCIRepositoryCmd = &cobra.Command{
 }
 
 type sourceOCIRepositoryFlags struct {
-	url         string
-	tag         string
-	digest      string
-	secretRef   string
-	ignorePaths []string
+	url            string
+	tag            string
+	semver         string
+	digest         string
+	secretRef      string
+	serviceAccount string
+	ignorePaths    []string
 }
 
 var sourceOCIRepositoryArgs = sourceOCIRepositoryFlags{}
@@ -63,8 +64,10 @@ var sourceOCIRepositoryArgs = sourceOCIRepositoryFlags{}
 func init() {
 	createSourceOCIRepositoryCmd.Flags().StringVar(&sourceOCIRepositoryArgs.url, "url", "", "the OCI repository URL")
 	createSourceOCIRepositoryCmd.Flags().StringVar(&sourceOCIRepositoryArgs.tag, "tag", "", "the OCI artifact tag")
+	createSourceOCIRepositoryCmd.Flags().StringVar(&sourceOCIRepositoryArgs.semver, "tag-semver", "", "the OCI artifact tag semver range")
 	createSourceOCIRepositoryCmd.Flags().StringVar(&sourceOCIRepositoryArgs.digest, "digest", "", "the OCI artifact digest")
-	createSourceOCIRepositoryCmd.Flags().StringVar(&sourceOCIRepositoryArgs.secretRef, "secret-ref", "", "the name of an existing secret containing credentials")
+	createSourceOCIRepositoryCmd.Flags().StringVar(&sourceOCIRepositoryArgs.secretRef, "secret-ref", "", "the name of the Kubernetes image pull secret (type 'kubernetes.io/dockerconfigjson')")
+	createSourceOCIRepositoryCmd.Flags().StringVar(&sourceOCIRepositoryArgs.secretRef, "service-account", "", "the name of the Kubernetes service account that refers to an image pull secret")
 	createSourceOCIRepositoryCmd.Flags().StringSliceVar(&sourceOCIRepositoryArgs.ignorePaths, "ignore-paths", nil, "set paths to ignore resources (can specify multiple paths with commas: path1,path2)")
 
 	createSourceCmd.AddCommand(createSourceOCIRepositoryCmd)
@@ -77,8 +80,8 @@ func createSourceOCIRepositoryCmdRun(cmd *cobra.Command, args []string) error {
 		return fmt.Errorf("url is required")
 	}
 
-	if sourceOCIRepositoryArgs.tag == "" && sourceOCIRepositoryArgs.digest == "" {
-		return fmt.Errorf("--tag or --digest is required")
+	if sourceOCIRepositoryArgs.semver == "" && sourceOCIRepositoryArgs.tag == "" && sourceOCIRepositoryArgs.digest == "" {
+		return fmt.Errorf("--tag, --tag-semver or --digest is required")
 	}
 
 	sourceLabels, err := parseLabels()
@@ -86,12 +89,6 @@ func createSourceOCIRepositoryCmdRun(cmd *cobra.Command, args []string) error {
 		return err
 	}
 
-	tmpDir, err := os.MkdirTemp("", name)
-	if err != nil {
-		return err
-	}
-	defer os.RemoveAll(tmpDir)
-
 	var ignorePaths *string
 	if len(sourceOCIRepositoryArgs.ignorePaths) > 0 {
 		ignorePathsStr := strings.Join(sourceOCIRepositoryArgs.ignorePaths, "\n")
@@ -114,20 +111,27 @@ func createSourceOCIRepositoryCmdRun(cmd *cobra.Command, args []string) error {
 		},
 	}
 
-	if sourceOCIRepositoryArgs.tag != "" {
-		repository.Spec.Reference.Tag = sourceOCIRepositoryArgs.tag
+	if digest := sourceOCIRepositoryArgs.digest; digest != "" {
+		repository.Spec.Reference.Digest = digest
 	}
-	if sourceOCIRepositoryArgs.digest != "" {
-		repository.Spec.Reference.Digest = sourceOCIRepositoryArgs.digest
+	if semver := sourceOCIRepositoryArgs.semver; semver != "" {
+		repository.Spec.Reference.SemVer = semver
+	}
+	if tag := sourceOCIRepositoryArgs.tag; tag != "" {
+		repository.Spec.Reference.Tag = tag
 	}
 
 	if createSourceArgs.fetchTimeout > 0 {
 		repository.Spec.Timeout = &metav1.Duration{Duration: createSourceArgs.fetchTimeout}
 	}
 
-	if sourceOCIRepositoryArgs.secretRef != "" {
+	if saName := sourceOCIRepositoryArgs.serviceAccount; saName != "" {
+		repository.Spec.ServiceAccountName = saName
+	}
+
+	if secretName := sourceOCIRepositoryArgs.secretRef; secretName != "" {
 		repository.Spec.SecretRef = &meta.LocalObjectReference{
-			Name: sourceOCIRepositoryArgs.secretRef,
+			Name: secretName,
 		}
 	}
 
diff --git a/cmd/flux/list.go b/cmd/flux/list.go
new file mode 100644
index 0000000000000000000000000000000000000000..e89dc149633b3064085b296373b0c4014c05e833
--- /dev/null
+++ b/cmd/flux/list.go
@@ -0,0 +1,31 @@
+/*
+Copyright 2021 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package main
+
+import (
+	"github.com/spf13/cobra"
+)
+
+var listCmd = &cobra.Command{
+	Use:   "list",
+	Short: "List artifacts",
+	Long:  "The list command is used for printing the OCI artifacts metadata.",
+}
+
+func init() {
+	rootCmd.AddCommand(listCmd)
+}
diff --git a/cmd/flux/list_artifact.go b/cmd/flux/list_artifact.go
new file mode 100644
index 0000000000000000000000000000000000000000..a53e4f9975d10edb825de31e5661fb711158ff6c
--- /dev/null
+++ b/cmd/flux/list_artifact.go
@@ -0,0 +1,68 @@
+/*
+Copyright 2022 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package main
+
+import (
+	"context"
+	"fmt"
+	"github.com/fluxcd/flux2/internal/oci"
+	"github.com/fluxcd/flux2/pkg/printers"
+	"github.com/spf13/cobra"
+)
+
+var listArtifactsCmd = &cobra.Command{
+	Use:   "artifacts",
+	Short: "list artifacts",
+	Long: `The list command fetches the tags and their metadata from a remote OCI repository.
+The list command uses the credentials from '~/.docker/config.json'.`,
+	Example: `# list the artifacts stored in an OCI repository
+flux list artifact ghcr.io/org/manifests/app
+`,
+	RunE: listArtifactsCmdRun,
+}
+
+func init() {
+	listCmd.AddCommand(listArtifactsCmd)
+}
+
+func listArtifactsCmdRun(cmd *cobra.Command, args []string) error {
+	if len(args) < 1 {
+		return fmt.Errorf("artifact repository is required")
+	}
+	url := args[0]
+
+	ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout)
+	defer cancel()
+
+	metas, err := oci.List(ctx, url)
+	if err != nil {
+		return err
+	}
+
+	var rows [][]string
+	for _, meta := range metas {
+		rows = append(rows, []string{meta.URL, meta.Digest, meta.Source, meta.Revision})
+	}
+
+	err = printers.TablePrinter([]string{"artifact", "digest", "source", "revision"}).Print(cmd.OutOrStdout(), rows)
+	if err != nil {
+		return err
+	}
+
+	return nil
+
+}
diff --git a/internal/oci/list.go b/internal/oci/list.go
new file mode 100644
index 0000000000000000000000000000000000000000..e827c472c227380ee4a82b1ca5774c20cee945f9
--- /dev/null
+++ b/internal/oci/list.go
@@ -0,0 +1,70 @@
+/*
+Copyright 2022 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package oci
+
+import (
+	"bytes"
+	"context"
+	"fmt"
+	"sort"
+	"strings"
+
+	"github.com/google/go-containerregistry/pkg/crane"
+	gcrv1 "github.com/google/go-containerregistry/pkg/v1"
+)
+
+// List fetches the tags and their manifests for a given OCI repository.
+func List(ctx context.Context, url string) ([]Metadata, error) {
+	metas := make([]Metadata, 0)
+	tags, err := crane.ListTags(url, craneOptions(ctx)...)
+	if err != nil {
+		return nil, fmt.Errorf("listing tags failed: %w", err)
+	}
+
+	sort.Slice(tags, func(i, j int) bool { return tags[i] > tags[j] })
+
+	for _, tag := range tags {
+		// exclude cosign signatures
+		if strings.HasSuffix(tag, ".sig") {
+			continue
+		}
+
+		meta := Metadata{
+			URL: fmt.Sprintf("%s:%s", url, tag),
+		}
+
+		manifestJSON, err := crane.Manifest(meta.URL, craneOptions(ctx)...)
+		if err != nil {
+			return nil, fmt.Errorf("fetching manifest failed: %w", err)
+		}
+
+		manifest, err := gcrv1.ParseManifest(bytes.NewReader(manifestJSON))
+		if err != nil {
+			return nil, fmt.Errorf("parsing manifest failed: %w", err)
+		}
+
+		meta.Digest = manifest.Config.Digest.String()
+		if m, err := MetadataFromAnnotations(manifest.Annotations); err == nil {
+			meta.Revision = m.Revision
+			meta.Source = m.Source
+		}
+
+		metas = append(metas, meta)
+	}
+
+	return metas, nil
+}
diff --git a/internal/oci/meta.go b/internal/oci/meta.go
index 3196cb26dd8f7fef30b735e3d43842ce230243fd..9e0c3c0ced9810f5b8b6f2568efdc67a57cf2381 100644
--- a/internal/oci/meta.go
+++ b/internal/oci/meta.go
@@ -29,6 +29,7 @@ type Metadata struct {
 	Source   string `json:"source_url"`
 	Revision string `json:"source_revision"`
 	Digest   string `json:"digest"`
+	URL      string `json:"url"`
 }
 
 func (m *Metadata) ToAnnotations() map[string]string {
@@ -40,7 +41,7 @@ func (m *Metadata) ToAnnotations() map[string]string {
 	return annotations
 }
 
-func GetMetadata(annotations map[string]string) (*Metadata, error) {
+func MetadataFromAnnotations(annotations map[string]string) (*Metadata, error) {
 	source, ok := annotations[SourceAnnotation]
 	if !ok {
 		return nil, fmt.Errorf("'%s' annotation not found", SourceAnnotation)
diff --git a/internal/oci/pull.go b/internal/oci/pull.go
index b37642a83ed357f1558ded37c1ae3a19469fb626..f08c0e345df684d84d9d7459ed59d783d382e806 100644
--- a/internal/oci/pull.go
+++ b/internal/oci/pull.go
@@ -47,7 +47,7 @@ func Pull(ctx context.Context, url, outDir string) (*Metadata, error) {
 		return nil, fmt.Errorf("parsing manifest failed: %w", err)
 	}
 
-	meta, err := GetMetadata(manifest.Annotations)
+	meta, err := MetadataFromAnnotations(manifest.Annotations)
 	if err != nil {
 		return nil, err
 	}