Repository: apigee/registry
Pull request #583 (2022-05-23 21:55:42)

Dry run support in compute command
I started this PR with adding `dry-run` support for score commands. 

However, it would be better to add that support across compute commands since each compute sub-command is uploading some artifact to the registry, users should have the option of not uploading the artifact.

codecov[bot]: # [Codecov](https://codecov.io/gh/apigee/registry/pull/583?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apigee) Report
> Merging [#583](https://codecov.io/gh/apigee/registry/pull/583?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apigee) (9886ffc) into [main](https://codecov.io/gh/apigee/registry/commit/da109e9df3158ffe82f4665bc769281b6db94737?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apigee) (da109e9) will **decrease** coverage by `0.33%`.
> The diff coverage is `12.77%`.

> :exclamation: Current head 9886ffc differs from pull request most recent head 65dbb3c. Consider uploading reports for the commit 65dbb3c to get more accurate results

```diff
@@            Coverage Diff             @@
##             main     #583      +/-   ##
==========================================
- Coverage   40.57%   40.23%   -0.34%     
==========================================
  Files          75       75              
  Lines        8956     9049      +93     
==========================================
+ Hits         3634     3641       +7     
- Misses       4901     4981      +80     
- Partials      421      427       +6     
```


| [Impacted Files](https://codecov.io/gh/apigee/registry/pull/583?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apigee) | Coverage Δ | |
|---|---|---|
| [cmd/registry/cmd/compute/complexity.go](https://codecov.io/gh/apigee/registry/pull/583/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apigee#diff-Y21kL3JlZ2lzdHJ5L2NtZC9jb21wdXRlL2NvbXBsZXhpdHkuZ28=) | `0.00% <0.00%> (ø)` | |
| [cmd/registry/cmd/compute/compute.go](https://codecov.io/gh/apigee/registry/pull/583/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apigee#diff-Y21kL3JlZ2lzdHJ5L2NtZC9jb21wdXRlL2NvbXB1dGUuZ28=) | `0.00% <0.00%> (ø)` | |
| [cmd/registry/cmd/compute/lint.go](https://codecov.io/gh/apigee/registry/pull/583/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apigee#diff-Y21kL3JlZ2lzdHJ5L2NtZC9jb21wdXRlL2xpbnQuZ28=) | `0.00% <0.00%> (ø)` | |
| [cmd/registry/cmd/compute/lintstats.go](https://codecov.io/gh/apigee/registry/pull/583/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apigee#diff-Y21kL3JlZ2lzdHJ5L2NtZC9jb21wdXRlL2xpbnRzdGF0cy5nbw==) | `0.00% <0.00%> (ø)` | |
| [cmd/registry/cmd/compute/references.go](https://codecov.io/gh/apigee/registry/pull/583/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apigee#diff-Y21kL3JlZ2lzdHJ5L2NtZC9jb21wdXRlL3JlZmVyZW5jZXMuZ28=) | `0.00% <0.00%> (ø)` | |
| [cmd/registry/cmd/compute/score.go](https://codecov.io/gh/apigee/registry/pull/583/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apigee#diff-Y21kL3JlZ2lzdHJ5L2NtZC9jb21wdXRlL3Njb3JlLmdv) | `0.00% <0.00%> (ø)` | |
| [cmd/registry/cmd/compute/scorecard.go](https://codecov.io/gh/apigee/registry/pull/583/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apigee#diff-Y21kL3JlZ2lzdHJ5L2NtZC9jb21wdXRlL3Njb3JlY2FyZC5nbw==) | `0.00% <0.00%> (ø)` | |
| [cmd/registry/cmd/compute/vocabulary.go](https://codecov.io/gh/apigee/registry/pull/583/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apigee#diff-Y21kL3JlZ2lzdHJ5L2NtZC9jb21wdXRlL3ZvY2FidWxhcnkuZ28=) | `0.00% <0.00%> (ø)` | |
| [cmd/registry/conformance/conformance-task.go](https://codecov.io/gh/apigee/registry/pull/583/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apigee#diff-Y21kL3JlZ2lzdHJ5L2NvbmZvcm1hbmNlL2NvbmZvcm1hbmNlLXRhc2suZ28=) | `46.10% <0.00%> (-1.43%)` | :arrow_down: |
| [cmd/registry/scoring/scorecard.go](https://codecov.io/gh/apigee/registry/pull/583/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apigee#diff-Y21kL3JlZ2lzdHJ5L3Njb3Jpbmcvc2NvcmVjYXJkLmdv) | `53.67% <27.27%> (-3.15%)` | :arrow_down: |
| ... and [10 more](https://codecov.io/gh/apigee/registry/pull/583/diff?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apigee) | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/apigee/registry/pull/583?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apigee).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apigee)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/apigee/registry/pull/583?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apigee). Last update [da109e9...65dbb3c](https://codecov.io/gh/apigee/registry/pull/583?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apigee). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apigee).


Patch:
diff --git a/cmd/registry/cmd/compute/complexity.go b/cmd/registry/cmd/compute/complexity.go
index 3de3c1c..da2066d 100644
--- a/cmd/registry/cmd/compute/complexity.go
+++ b/cmd/registry/cmd/compute/complexity.go
@@ -43,6 +43,10 @@ func complexityCommand() *cobra.Command {
 			if err != nil {
 				log.FromContext(ctx).WithError(err).Fatal("Failed to get filter from flags")
 			}
+			dryRun, err := cmd.Flags().GetBool("dry-run")
+			if err != nil {
+				log.FromContext(ctx).WithError(err).Fatal("Failed to get dry-run from flags")
+			}
 
 			client, err := connection.NewClient(ctx)
 			if err != nil {
@@ -62,6 +66,7 @@ func complexityCommand() *cobra.Command {
 				taskQueue <- &computeComplexityTask{
 					client:   client,
 					specName: spec.Name,
+					dryRun:   dryRun,
 				}
 				return nil
 			})
@@ -75,6 +80,7 @@ func complexityCommand() *cobra.Command {
 type computeComplexityTask struct {
 	client   connection.Client
 	specName string
+	dryRun   bool
 }
 
 func (task *computeComplexityTask) String() string {
@@ -122,6 +128,11 @@ func (task *computeComplexityTask) Run(ctx context.Context) error {
 	} else {
 		return fmt.Errorf("we don't know how to summarize %s", task.specName)
 	}
+
+	if task.dryRun {
+		core.PrintMessage(complexity)
+		return nil
+	}
 	subject := task.specName
 	messageData, _ := proto.Marshal(complexity)
 	artifact := &rpc.Artifact{
@@ -129,9 +140,5 @@ func (task *computeComplexityTask) Run(ctx context.Context) error {
 		MimeType: core.MimeTypeForMessageType("gnostic.metrics.Complexity"),
 		Contents: messageData,
 	}
-	err = core.SetArtifact(ctx, task.client, artifact)
-	if err != nil {
-		return err
-	}
-	return nil
+	return core.SetArtifact(ctx, task.client, artifact)
 }
diff --git a/cmd/registry/cmd/compute/compute.go b/cmd/registry/cmd/compute/compute.go
index 40b1c89..5c52333 100644
--- a/cmd/registry/cmd/compute/compute.go
+++ b/cmd/registry/cmd/compute/compute.go
@@ -34,5 +34,6 @@ func Command() *cobra.Command {
 	cmd.AddCommand(vocabularyCommand())
 
 	cmd.PersistentFlags().String("filter", "", "Filter selected resources")
+	cmd.PersistentFlags().Bool("dry-run", false, "if set, computation results will only be printed and will not stored in the registry")
 	return cmd
 }
diff --git a/cmd/registry/cmd/compute/conformance.go b/cmd/registry/cmd/compute/conformance.go
index 0a76e81..cffc933 100644
--- a/cmd/registry/cmd/compute/conformance.go
+++ b/cmd/registry/cmd/compute/conformance.go
@@ -30,14 +30,21 @@ import (
 const styleguideFilter = "mime_type.contains('google.cloud.apigeeregistry.applications.v1alpha1.StyleGuide')"
 
 func conformanceCommand() *cobra.Command {
-	var filter string
-
 	cmd := &cobra.Command{
 		Use:   "conformance",
 		Short: "Compute lint results for API specs",
 		Args:  cobra.MinimumNArgs(1),
 		Run: func(cmd *cobra.Command, args []string) {
 			ctx := cmd.Context()
+			filter, err := cmd.Flags().GetString("filter")
+			if err != nil {
+				log.FromContext(ctx).WithError(err).Fatal("Failed to get filter from flags")
+			}
+			dryRun, err := cmd.Flags().GetBool("dry-run")
+			if err != nil {
+				log.FromContext(ctx).WithError(err).Fatal("Failed to get dry-run from flags")
+			}
+
 			client, err := connection.NewClient(ctx)
 			if err != nil {
 				log.FromContext(ctx).WithError(err).Fatal("Failed to get client")
@@ -71,18 +78,17 @@ func conformanceCommand() *cobra.Command {
 
 			for _, guide := range guides {
 				log.Debugf(ctx, "Processing styleguide: %s", guide.GetId())
-				processStyleGuide(ctx, client, guide, specs)
+				processStyleGuide(ctx, client, guide, specs, dryRun)
 			}
 		},
 	}
 
-	cmd.Flags().StringVar(&filter, "filter", "", "Filter selected resources")
 	return cmd
 }
 
 // processStyleGuide computes and attaches conformance reports as
 // artifacts to a spec or a collection of specs.
-func processStyleGuide(ctx context.Context, client connection.Client, styleguide *rpc.StyleGuide, specs []*rpc.ApiSpec) {
+func processStyleGuide(ctx context.Context, client connection.Client, styleguide *rpc.StyleGuide, specs []*rpc.ApiSpec, dryRun bool) {
 	linterNameToMetadata, err := conformance.GenerateLinterMetadata(styleguide)
 	if err != nil {
 		log.Errorf(ctx, "Failed generating linter metadata, check styleguide definition, Error: %s", err)
@@ -98,19 +104,13 @@ func processStyleGuide(ctx context.Context, client connection.Client, styleguide
 				continue // Only compute matching style guides.
 			}
 
-			taskQueue <- &conformance.ComputeConformanceTask{
-				Client:          client,
-				Spec:            spec,
-				LintersMetadata: linterNameToMetadata,
-				StyleguideId:    styleguide.GetId(),
-			}
-
 			// Delegate the task of computing the conformance report for this spec to the worker pool.
 			taskQueue <- &conformance.ComputeConformanceTask{
 				Client:          client,
 				Spec:            spec,
 				LintersMetadata: linterNameToMetadata,
 				StyleguideId:    styleguide.GetId(),
+				DryRun:          dryRun,
 			}
 		}
 	}
diff --git a/cmd/registry/cmd/compute/lint.go b/cmd/registry/cmd/compute/lint.go
index ad5ce51..9f5faea 100644
--- a/cmd/registry/cmd/compute/lint.go
+++ b/cmd/registry/cmd/compute/lint.go
@@ -39,6 +39,10 @@ func lintCommand() *cobra.Command {
 			if err != nil {
 				log.FromContext(ctx).WithError(err).Fatal("Failed to get filter from flags")
 			}
+			dryRun, err := cmd.Flags().GetBool("dry-run")
+			if err != nil {
+				log.FromContext(ctx).WithError(err).Fatal("Failed to get fry-run from flags")
+			}
 
 			client, err := connection.NewClient(ctx)
 			if err != nil {
@@ -59,6 +63,7 @@ func lintCommand() *cobra.Command {
 					client:   client,
 					specName: spec.Name,
 					linter:   linter,
+					dryRun:   dryRun,
 				}
 				return nil
 			})
@@ -76,6 +81,7 @@ type computeLintTask struct {
 	client   connection.Client
 	specName string
 	linter   string
+	dryRun   bool
 }
 
 func (task *computeLintTask) String() string {
@@ -127,6 +133,12 @@ func (task *computeLintTask) Run(ctx context.Context) error {
 	} else {
 		return fmt.Errorf("we don't know how to lint %s", spec.Name)
 	}
+
+	if task.dryRun {
+		core.PrintMessage(lint)
+		return nil
+	}
+
 	subject := spec.GetName()
 	messageData, _ := proto.Marshal(lint)
 	artifact := &rpc.Artifact{
@@ -134,9 +146,5 @@ func (task *computeLintTask) Run(ctx context.Context) error {
 		MimeType: core.MimeTypeForMessageType("google.cloud.apigeeregistry.applications.v1alpha1.Lint"),
 		Contents: messageData,
 	}
-	err = core.SetArtifact(ctx, task.client, artifact)
-	if err != nil {
-		return err
-	}
-	return nil
+	return core.SetArtifact(ctx, task.client, artifact)
 }
diff --git a/cmd/registry/cmd/compute/lintstats.go b/cmd/registry/cmd/compute/lintstats.go
index 66de114..f6eefd3 100644
--- a/cmd/registry/cmd/compute/lintstats.go
+++ b/cmd/registry/cmd/compute/lintstats.go
@@ -47,6 +47,10 @@ func lintStatsCommand() *cobra.Command {
 			if err != nil {
 				log.FromContext(ctx).WithError(err).Fatal("Failed to get filter from flags")
 			}
+			dryRun, err := cmd.Flags().GetBool("dry-run")
+			if err != nil {
+				log.FromContext(ctx).WithError(err).Fatal("Failed to get dry-run from flags")
+			}
 
 			client, err := connection.NewClient(ctx)
 			if err != nil {
@@ -58,7 +62,7 @@ func lintStatsCommand() *cobra.Command {
 				log.FromContext(ctx).WithError(err).Fatal("Failed to get client")
 			}
 
-			err = matchAndHandleLintStatsCmd(ctx, client, adminClient, args[0], filter, linter)
+			err = matchAndHandleLintStatsCmd(ctx, client, adminClient, args[0], filter, linter, dryRun)
 			if err != nil {
 				log.FromContext(ctx).WithError(err).Fatal("Failed to match or handle command")
 			}
@@ -103,7 +107,8 @@ func computeLintStatsSpecs(ctx context.Context,
 	client *gapic.RegistryClient,
 	spec names.Spec,
 	filter string,
-	linter string) error {
+	linter string,
+	dryRun bool) error {
 	return core.ListSpecs(ctx, client, spec, filter, func(spec *rpc.ApiSpec) error {
 		// Iterate through a collection of specs and evaluate each.
 		log.Debug(ctx, spec.GetName())
@@ -154,7 +159,11 @@ func computeLintStatsSpecs(ctx context.Context,
 			lintStats.SchemaCount = complexity.GetSchemaCount()
 		}
 
-		_ = storeLintStatsArtifact(ctx, client, spec.GetName(), linter, lintStats)
+		if dryRun {
+			core.PrintMessage(lintStats)
+		} else {
+			_ = storeLintStatsArtifact(ctx, client, spec.GetName(), linter, lintStats)
+		}
 		return nil
 	})
 }
@@ -164,7 +173,8 @@ func computeLintStatsProjects(ctx context.Context,
 	adminClient *gapic.AdminClient,
 	projectName names.Project,
 	filter string,
-	linter string) error {
+	linter string,
+	dryRun bool) error {
 	return core.ListProjects(ctx, adminClient, projectName, filter, func(project *rpc.Project) error {
 		project_stats := &rpc.LintStats{}
 
@@ -174,9 +184,13 @@ func computeLintStatsProjects(ctx context.Context,
 		}); err != nil {
 			return nil
 		}
-		// Store the aggregate stats on this project
-		_ = storeLintStatsArtifact(ctx, client, project.GetName()+"/locations/global", linter, project_stats)
-		log.Debug(ctx, project.GetName())
+		if dryRun {
+			core.PrintMessage(project_stats)
+		} else {
+			// Store the aggregate stats on this project
+			_ = storeLintStatsArtifact(ctx, client, project.GetName()+"/locations/global", linter, project_stats)
+			log.Debug(ctx, project.GetName())
+		}
 		return nil
 	})
 }
@@ -185,7 +199,8 @@ func computeLintStatsAPIs(ctx context.Context,
 	client *gapic.RegistryClient,
 	apiName names.Api,
 	filter string,
-	linter string) error {
+	linter string,
+	dryRun bool) error {
 	return core.ListAPIs(ctx, client, apiName, filter, func(api *rpc.Api) error {
 		api_stats := &rpc.LintStats{}
 
@@ -195,9 +210,14 @@ func computeLintStatsAPIs(ctx context.Context,
 		}); err != nil {
 			return nil
 		}
-		// Store the aggregate stats on this api
-		_ = storeLintStatsArtifact(ctx, client, api.GetName(), linter, api_stats)
-		log.Debug(ctx, api.GetName())
+
+		if dryRun {
+			core.PrintMessage(api_stats)
+		} else {
+			// Store the aggregate stats on this api
+			_ = storeLintStatsArtifact(ctx, client, api.GetName(), linter, api_stats)
+			log.Debug(ctx, api.GetName())
+		}
 		return nil
 	})
 }
@@ -206,7 +226,8 @@ func computeLintStatsVersions(ctx context.Context,
 	client *gapic.RegistryClient,
 	versionName names.Version,
 	filter string,
-	linter string) error {
+	linter string,
+	dryRun bool) error {
 	return core.ListVersions(ctx, client, versionName, filter, func(version *rpc.ApiVersion) error {
 		stats := &rpc.LintStats{}
 		if err := core.ListSpecs(ctx, client, versionName.Spec(""), filter, func(spec *rpc.ApiSpec) error {
@@ -215,9 +236,14 @@ func computeLintStatsVersions(ctx context.Context,
 		}); err != nil {
 			return nil
 		}
-		// Store the aggregate stats on this version
-		_ = storeLintStatsArtifact(ctx, client, version.GetName(), linter, stats)
-		log.Debug(ctx, version.GetName())
+
+		if dryRun {
+			core.PrintMessage(stats)
+		} else {
+			// Store the aggregate stats on this version
+			_ = storeLintStatsArtifact(ctx, client, version.GetName(), linter, stats)
+			log.Debug(ctx, version.GetName())
+		}
 		return nil
 	})
 }
@@ -269,24 +295,25 @@ func matchAndHandleLintStatsCmd(
 	name string,
 	filter string,
 	linter string,
+	dryRun bool,
 ) error {
 	// First try to match collection names, then try to match resource names.
 	if project, err := names.ParseProjectCollection(name); err == nil {
-		return computeLintStatsProjects(ctx, client, adminClient, project, filter, linter)
+		return computeLintStatsProjects(ctx, client, adminClient, project, filter, linter, dryRun)
 	} else if api, err := names.ParseApiCollection(name); err == nil {
-		return computeLintStatsAPIs(ctx, client, api, filter, linter)
+		return computeLintStatsAPIs(ctx, client, api, filter, linter, dryRun)
 	} else if version, err := names.ParseVersionCollection(name); err == nil {
-		return computeLintStatsVersions(ctx, client, version, filter, linter)
+		return computeLintStatsVersions(ctx, client, version, filter, linter, dryRun)
 	} else if spec, err := names.ParseSpecCollection(name); err == nil {
-		return computeLintStatsSpecs(ctx, client, spec, filter, linter)
+		return computeLintStatsSpecs(ctx, client, spec, filter, linter, dryRun)
 	} else if project, err := names.ParseProject(name); err == nil {
-		return computeLintStatsProjects(ctx, client, adminClient, project, filter, linter)
+		return computeLintStatsProjects(ctx, client, adminClient, project, filter, linter, dryRun)
 	} else if api, err := names.ParseApi(name); err == nil {
-		return computeLintStatsAPIs(ctx, client, api, filter, linter)
+		return computeLintStatsAPIs(ctx, client, api, filter, linter, dryRun)
 	} else if version, err := names.ParseVersion(name); err == nil {
-		return computeLintStatsVersions(ctx, client, version, filter, linter)
+		return computeLintStatsVersions(ctx, client, version, filter, linter, dryRun)
 	} else if spec, err := names.ParseSpec(name); err == nil {
-		return computeLintStatsSpecs(ctx, client, spec, filter, linter)
+		return computeLintStatsSpecs(ctx, client, spec, filter, linter, dryRun)
 	} else {
 		// If nothing matched, return an error.
 		return fmt.Errorf("unsupported argument: %s", name)
diff --git a/cmd/registry/cmd/compute/references.go b/cmd/registry/cmd/compute/references.go
index 06bdb46..8cff25b 100644
--- a/cmd/registry/cmd/compute/references.go
+++ b/cmd/registry/cmd/compute/references.go
@@ -38,6 +38,10 @@ func referencesCommand() *cobra.Command {
 			if err != nil {
 				log.FromContext(ctx).WithError(err).Fatal("Failed to get filter from flags")
 			}
+			dryRun, err := cmd.Flags().GetBool("dry-run")
+			if err != nil {
+				log.FromContext(ctx).WithError(err).Fatal("Failed to get dry-run from flags")
+			}
 
 			client, err := connection.NewClient(ctx)
 			if err != nil {
@@ -57,6 +61,7 @@ func referencesCommand() *cobra.Command {
 				taskQueue <- &computeReferencesTask{
 					client:   client,
 					specName: spec.Name,
+					dryRun:   dryRun,
 				}
 				return nil
 			})
@@ -70,6 +75,7 @@ func referencesCommand() *cobra.Command {
 type computeReferencesTask struct {
 	client   connection.Client
 	specName string
+	dryRun   bool
 }
 
 func (task *computeReferencesTask) String() string {
@@ -96,6 +102,11 @@ func (task *computeReferencesTask) Run(ctx context.Context) error {
 		return fmt.Errorf("we don't know how to compute references for %s of type %s", task.specName, contents.GetContentType())
 	}
 
+	if task.dryRun {
+		core.PrintMessage(references)
+		return nil
+	}
+
 	messageData, _ := proto.Marshal(references)
 	artifact := &rpc.Artifact{
 		Name:     task.specName + "/artifacts/references",
diff --git a/cmd/registry/cmd/compute/score.go b/cmd/registry/cmd/compute/score.go
index ce20c6d..db40c99 100644
--- a/cmd/registry/cmd/compute/score.go
+++ b/cmd/registry/cmd/compute/score.go
@@ -33,10 +33,15 @@ func scoreCommand() *cobra.Command {
 		Args:  cobra.ExactArgs(1),
 		Run: func(cmd *cobra.Command, args []string) {
 			ctx := cmd.Context()
+
 			filter, err := cmd.Flags().GetString("filter")
 			if err != nil {
 				log.FromContext(ctx).WithError(err).Fatal("Failed to get filter from flags")
 			}
+			dryRun, err := cmd.Flags().GetBool("dry-run")
+			if err != nil {
+				log.FromContext(ctx).WithError(err).Fatal("Failed to get dry-run from flags")
+			}
 
 			client, err := connection.NewClient(ctx)
 			if err != nil {
@@ -63,6 +68,7 @@ func scoreCommand() *cobra.Command {
 						client:      client,
 						defArtifact: d,
 						resource:    r,
+						dryRun:      dryRun,
 					}
 				}
 			}
@@ -76,6 +82,7 @@ type computeScoreTask struct {
 	client      connection.Client
 	defArtifact *rpc.Artifact
 	resource    patterns.ResourceInstance
+	dryRun      bool
 }
 
 func (task *computeScoreTask) String() string {
@@ -83,5 +90,5 @@ func (task *computeScoreTask) String() string {
 }
 
 func (task *computeScoreTask) Run(ctx context.Context) error {
-	return scoring.CalculateScore(ctx, task.client, task.defArtifact, task.resource)
+	return scoring.CalculateScore(ctx, task.client, task.defArtifact, task.resource, task.dryRun)
 }
diff --git a/cmd/registry/cmd/compute/scorecard.go b/cmd/registry/cmd/compute/scorecard.go
index ecadefe..78277bb 100644
--- a/cmd/registry/cmd/compute/scorecard.go
+++ b/cmd/registry/cmd/compute/scorecard.go
@@ -37,6 +37,10 @@ func scoreCardCommand() *cobra.Command {
 			if err != nil {
 				log.FromContext(ctx).WithError(err).Fatal("Failed to get filter from flags")
 			}
+			dryRun, err := cmd.Flags().GetBool("dry-run")
+			if err != nil {
+				log.FromContext(ctx).WithError(err).Fatal("Failed to get dry-run from flags")
+			}
 
 			client, err := connection.NewClient(ctx)
 			if err != nil {
@@ -63,6 +67,7 @@ func scoreCardCommand() *cobra.Command {
 						client:      client,
 						defArtifact: d,
 						resource:    r,
+						dryRun:      dryRun,
 					}
 				}
 			}
@@ -76,6 +81,7 @@ type computeScoreCardTask struct {
 	client      connection.Client
 	defArtifact *rpc.Artifact
 	resource    patterns.ResourceInstance
+	dryRun      bool
 }
 
 func (task *computeScoreCardTask) String() string {
@@ -83,5 +89,5 @@ func (task *computeScoreCardTask) String() string {
 }
 
 func (task *computeScoreCardTask) Run(ctx context.Context) error {
-	return scoring.CalculateScoreCard(ctx, task.client, task.defArtifact, task.resource)
+	return scoring.CalculateScoreCard(ctx, task.client, task.defArtifact, task.resource, task.dryRun)
 }
diff --git a/cmd/registry/cmd/compute/vocabulary.go b/cmd/registry/cmd/compute/vocabulary.go
index 8f25fc7..70edc67 100644
--- a/cmd/registry/cmd/compute/vocabulary.go
+++ b/cmd/registry/cmd/compute/vocabulary.go
@@ -44,6 +44,10 @@ func vocabularyCommand() *cobra.Command {
 			if err != nil {
 				log.FromContext(ctx).WithError(err).Fatal("Failed to get filter from flags")
 			}
+			dryRun, err := cmd.Flags().GetBool("dry-run")
+			if err != nil {
+				log.FromContext(ctx).WithError(err).Fatal("Failed to get dry-run from flags")
+			}
 
 			client, err := connection.NewClient(ctx)
 			if err != nil {
@@ -63,6 +67,7 @@ func vocabularyCommand() *cobra.Command {
 				taskQueue <- &computeVocabularyTask{
 					client:   client,
 					specName: spec.GetName(),
+					dryRun:   dryRun,
 				}
 				return nil
 			}); err != nil {
@@ -75,6 +80,7 @@ func vocabularyCommand() *cobra.Command {
 type computeVocabularyTask struct {
 	client   connection.Client
 	specName string
+	dryRun   bool
 }
 
 func (task *computeVocabularyTask) String() string {
@@ -123,11 +129,15 @@ func (task *computeVocabularyTask) Run(ctx context.Context) error {
 		return fmt.Errorf("we don't know how to summarize %s", task.specName)
 	}
 
+	if task.dryRun {
+		core.PrintMessage(vocab)
+		return nil
+	}
+
 	messageData, err := proto.Marshal(vocab)
 	if err != nil {
 		return err
 	}
-
 	return core.SetArtifact(ctx, task.client, &rpc.Artifact{
 		Name:     task.specName + "/artifacts/vocabulary",
 		MimeType: core.MimeTypeForMessageType("gnostic.metrics.Vocabulary"),
diff --git a/cmd/registry/conformance/conformance-task.go b/cmd/registry/conformance/conformance-task.go
index d225b34..9a7f6ff 100644
--- a/cmd/registry/conformance/conformance-task.go
+++ b/cmd/registry/conformance/conformance-task.go
@@ -79,6 +79,7 @@ type ComputeConformanceTask struct {
 	Spec            *rpc.ApiSpec
 	LintersMetadata map[string]*linterMetadata
 	StyleguideId    string
+	DryRun          bool
 }
 
 func (task *ComputeConformanceTask) String() string {
@@ -126,6 +127,10 @@ func (task *ComputeConformanceTask) Run(ctx context.Context) error {
 		task.computeConformanceReport(ctx, conformanceReport, guidelineReportsMap, linterResponse, metadata)
 	}
 
+	if task.DryRun {
+		core.PrintMessage(conformanceReport)
+		return nil
+	}
 	return task.storeConformanceReport(ctx, conformanceReport)
 }
 
diff --git a/cmd/registry/scoring/score.go b/cmd/registry/scoring/score.go
index 997ccac..f4272c9 100644
--- a/cmd/registry/scoring/score.go
+++ b/cmd/registry/scoring/score.go
@@ -76,7 +76,8 @@ func CalculateScore(
 	ctx context.Context,
 	client connection.Client,
 	defArtifact *rpc.Artifact,
-	resource patterns.ResourceInstance) error {
+	resource patterns.ResourceInstance,
+	dryRun bool) error {
 	project := fmt.Sprintf("%s/locations/global", resource.ResourceName().Project())
 
 	// Extract definition
@@ -107,7 +108,7 @@ func CalculateScore(
 	// evaluate the expression and return a scoreValue
 	result := processFormula(ctx, client, definition, resource, scoreArtifact, takeAction)
 	if result.err != nil {
-		return err
+		return result.err
 	}
 
 	if result.needsUpdate {
@@ -117,14 +118,14 @@ func CalculateScore(
 			return err
 		}
 
-		// TODO: Add dry_run flag
-
-		err = uploadScore(ctx, client, resource, score)
-		if err != nil {
-			return err
+		if dryRun {
+			core.PrintMessage(score)
+			return nil
 		}
+		return uploadScore(ctx, client, resource, score)
 	}
 
+	log.Debugf(ctx, "Score %s is already up-to-date.", artifactName)
 	return nil
 }
 
diff --git a/cmd/registry/scoring/scorecard.go b/cmd/registry/scoring/scorecard.go
index f78ac5c..26d4ba4 100644
--- a/cmd/registry/scoring/scorecard.go
+++ b/cmd/registry/scoring/scorecard.go
@@ -74,7 +74,8 @@ func CalculateScoreCard(
 	ctx context.Context,
 	client connection.Client,
 	defArtifact *rpc.Artifact,
-	resource patterns.ResourceInstance) error {
+	resource patterns.ResourceInstance,
+	dryRun bool) error {
 	project := fmt.Sprintf("%s/locations/global", resource.ResourceName().Project())
 
 	// Extract definition
@@ -108,15 +109,15 @@ func CalculateScoreCard(
 	}
 
 	if result.needsUpdate {
-		// TODO: Add dry_run flag
-
-		// upload the scoreCard to registry
-		err = uploadScoreCard(ctx, client, resource, result.scoreCard)
-		if err != nil {
-			return err
+		if dryRun {
+			core.PrintMessage(result.scoreCard)
+			return nil
 		}
+		// upload the scoreCard to registry
+		return uploadScoreCard(ctx, client, resource, result.scoreCard)
 	}
 
+	log.Debugf(ctx, "ScoreCard %s is already up-to-date.", artifactName)
 	return nil
 }
 


Test patch:
diff --git a/cmd/registry/cmd/compute/conformance_test.go b/cmd/registry/cmd/compute/conformance_test.go
index 4b3fb15..ccd2b91 100644
--- a/cmd/registry/cmd/compute/conformance_test.go
+++ b/cmd/registry/cmd/compute/conformance_test.go
@@ -427,7 +427,10 @@ func TestConformance(t *testing.T) {
 				t.Fatalf("Failed to upload the styleguide: %s", err)
 			}
 
+			// setup the command
 			conformanceCmd := conformanceCommand()
+			conformanceCmd.Flags().String("filter", "", "Filter selected resources")
+			conformanceCmd.Flags().Bool("dry-run", false, "if set, computation results will only be printed and not uploaded to registry")
 
 			args = []string{spec.Name}
 			conformanceCmd.SetArgs(args)
diff --git a/cmd/registry/scoring/score_test.go b/cmd/registry/scoring/score_test.go
index cab54e0..d77a3da 100644
--- a/cmd/registry/scoring/score_test.go
+++ b/cmd/registry/scoring/score_test.go
@@ -498,7 +498,7 @@ func TestCalculateScore(t *testing.T) {
 				t.Errorf("failed to fetch the definition Artifact from setup: %s", err)
 			}
 
-			gotErr := CalculateScore(ctx, registryClient, defArtifact, resource)
+			gotErr := CalculateScore(ctx, registryClient, defArtifact, resource, false)
 			if gotErr != nil {
 				t.Errorf("CalculateScore(ctx, client, %v, %v) returned unexpected error: %s", defArtifact, resource, gotErr)
 			}
diff --git a/cmd/registry/scoring/scorecard_test.go b/cmd/registry/scoring/scorecard_test.go
index dcf2012..ab0a965 100644
--- a/cmd/registry/scoring/scorecard_test.go
+++ b/cmd/registry/scoring/scorecard_test.go
@@ -533,7 +533,7 @@ func TestCalculateScoreCard(t *testing.T) {
 				t.Errorf("failed to fetch the definition Artifact from setup: %s", err)
 			}
 
-			gotErr := CalculateScoreCard(ctx, registryClient, defArtifact, resource)
+			gotErr := CalculateScoreCard(ctx, registryClient, defArtifact, resource, false)
 			if gotErr != nil {
 				t.Errorf("CalculateScore(ctx, client, %v, %v) returned unexpected error: %s", defArtifact, resource, gotErr)
 			}