Repository: apigee/registry
Pull request #607 (2022-06-13 19:25:10)

Implement the behavior of "refresh" field in manifest definition


Patch:
diff --git a/cmd/registry/controller/controller.go b/cmd/registry/controller/controller.go
index 5634ece..a5045aa 100644
--- a/cmd/registry/controller/controller.go
+++ b/cmd/registry/controller/controller.go
@@ -39,7 +39,7 @@ func ProcessManifest(
 	manifest *rpc.Manifest) []*Action {
 	var actions []*Action
 	//Check for errors in manifest
-	errs := ValidateManifest(fmt.Sprintf("projects/%s", projectID), manifest)
+	errs := ValidateManifest(fmt.Sprintf("projects/%s/locations/global", projectID), manifest)
 	if len(errs) > 0 {
 		for _, err := range errs {
 			log.FromContext(ctx).WithError(err).Debugf("Error in manifest")
@@ -71,8 +71,8 @@ func processManifestResource(
 	client connection.Client,
 	projectID string,
 	generatedResource *rpc.GeneratedResource) ([]*Action, error) {
-	// Generate dependency map
 	resourcePattern := fmt.Sprintf("projects/%s/locations/global/%s", projectID, generatedResource.Pattern)
+	// Generate dependency map
 	dependencyMaps := make([]map[string]time.Time, 0, len(generatedResource.Dependencies))
 	for _, dependency := range generatedResource.Dependencies {
 		dMap, err := generateDependencyMap(ctx, client, resourcePattern, dependency)
@@ -152,20 +152,17 @@ func generateActions(
 	generatedResource *rpc.GeneratedResource) []*Action {
 	actions := make([]*Action, 0)
 
-	// Calculate actions only if dependencies are non-empty
-	if len(dependencyMaps) > 0 {
-		updateActions, visited, err := generateUpdateActions(ctx, client, resourcePattern, filter, dependencyMaps, generatedResource)
-		if err != nil {
-			log.Errorf(ctx, "Error while generating UpdateActions: %s", err)
-		}
-		actions = append(actions, updateActions...)
+	updateActions, visited, err := generateUpdateActions(ctx, client, resourcePattern, filter, dependencyMaps, generatedResource)
+	if err != nil {
+		log.Errorf(ctx, "Error while generating UpdateActions: %s", err)
+	}
+	actions = append(actions, updateActions...)
 
-		createActions, err := generateCreateActions(ctx, client, resourcePattern, dependencyMaps, generatedResource, visited)
-		if err != nil {
-			log.Errorf(ctx, "Error while generating CreateActions: %s", err)
-		}
-		actions = append(actions, createActions...)
+	createActions, err := generateCreateActions(ctx, client, resourcePattern, dependencyMaps, generatedResource, visited)
+	if err != nil {
+		log.Errorf(ctx, "Error while generating CreateActions: %s", err)
 	}
+	actions = append(actions, createActions...)
 
 	return actions
 }
@@ -331,6 +328,11 @@ func needsUpdate(
 	targetResourceTime time.Time,
 	dependencyMaps []map[string]time.Time,
 	generatedResource *rpc.GeneratedResource) (bool, error) {
+	// Check "refresh" first to decide whether to take action or not.
+	if generatedResource.Refresh != nil && targetResourceTime.Add(generatedResource.Refresh.AsDuration()).Before(time.Now()) {
+		return true, nil
+	}
+	// Check for dependencies otherwise
 	for i, dependency := range generatedResource.Dependencies {
 		dMap := dependencyMaps[i]
 		// Get the entity to look for in dependencyMap
@@ -357,6 +359,11 @@ func needsCreate(
 	targetResourceName patterns.ResourceName,
 	dependencyMaps []map[string]time.Time,
 	generatedResource *rpc.GeneratedResource) (bool, error) {
+	// Take action if "refresh" is set and > 0
+	if generatedResource.Refresh != nil && generatedResource.Refresh.AsDuration().Seconds() > 0 {
+		return true, nil
+	}
+	// Check for dependencies otherwise
 	for i, dependency := range generatedResource.Dependencies {
 		dMap := dependencyMaps[i]
 		// Get the entity to look for in dependencyMap
diff --git a/cmd/registry/controller/parser.go b/cmd/registry/controller/parser.go
index 617c681..2a542f8 100644
--- a/cmd/registry/controller/parser.go
+++ b/cmd/registry/controller/parser.go
@@ -83,6 +83,16 @@ func validateGeneratedResourceEntry(parent string, generatedResource *rpc.Genera
 		}
 	}
 
+	// Check that either "dependencies" or "refresh" is set and "refresh > 0"
+	if len(generatedResource.Dependencies) == 0 && generatedResource.Refresh == nil {
+		errs = append(errs, fmt.Errorf("either 'dependencies' or 'refresh' must be set for generated resource: %v", generatedResource))
+	}
+
+	// Check that "refresh" > 0
+	if generatedResource.Refresh != nil && generatedResource.Refresh.AsDuration().Seconds() == 0 {
+		errs = append(errs, fmt.Errorf("'refresh' must be >0 for generated resource: %v", generatedResource))
+	}
+
 	//Validate that all the action References are valid
 	references, err := getReferencesFromAction(generatedResource.Action)
 	if err != nil {
diff --git a/google/cloud/apigeeregistry/v1/controller/manifest.proto b/google/cloud/apigeeregistry/v1/controller/manifest.proto
index e7576fb..22061e1 100644
--- a/google/cloud/apigeeregistry/v1/controller/manifest.proto
+++ b/google/cloud/apigeeregistry/v1/controller/manifest.proto
@@ -17,6 +17,7 @@ syntax = "proto3";
 package google.cloud.apigeeregistry.v1.controller;
 
 import "google/api/field_behavior.proto";
+import "google/protobuf/duration.proto";
 
 option java_package = "com.google.cloud.apigeeregistry.v1.controller";
 option java_multiple_files = true;
@@ -67,12 +68,28 @@ message GeneratedResource {
   bool receipt = 3;
 
   // The dependencies of the resource.
+  // The controller checks for any updates in the dependencies and takes an
+  // action if there has been an update in dependencies from the last time
+  // the action was taken.
+  // Check "refresh" field if it is not possible to list out the dependencies.
+  // Either "dependencies" or "refresh" must be set for the controller to work.
   repeated Dependency dependencies = 4;
 
   // The action used to generate the resource.
-  // An action can contain references to both the resource and dependencies
-  // Example: "registry compute complexity $dependency0 $resource"
+  // An action can contain references to $resource
+  // Example: "registry compute complexity $resource"
   string action = 5 [(google.api.field_behavior) = REQUIRED];
+
+  // Refresh interval in seconds (must be >0).
+  // Generated resource should be regenerated after the specified duration.
+  // This field should be used when the dependencies of the generated resources
+  // are dynamic and cannot be listed out in the manifest.
+  // If "refresh" is set, the dependencies are ignored and the
+  // controller will use this field to decide when to take the action.
+  // If refresh=Ns, the controller will wait for at least N seconds
+  // before trying to regenerate the generated resource.
+  // Either "refresh" or "dependencies" must be set for the controller to work.
+  google.protobuf.Duration refresh = 6;
 }
 
 // A dependency of a generated resource is another resource in the registry
diff --git a/rpc/manifest.pb.go b/rpc/manifest.pb.go
index 77c06b5..f98b1e3 100644
--- a/rpc/manifest.pb.go
+++ b/rpc/manifest.pb.go
@@ -24,6 +24,7 @@ import (
 	_ "google.golang.org/genproto/googleapis/api/annotations"
 	protoreflect "google.golang.org/protobuf/reflect/protoreflect"
 	protoimpl "google.golang.org/protobuf/runtime/protoimpl"
+	durationpb "google.golang.org/protobuf/types/known/durationpb"
 	reflect "reflect"
 	sync "sync"
 )
@@ -146,11 +147,26 @@ type GeneratedResource struct {
 	// generated resource.
 	Receipt bool `protobuf:"varint,3,opt,name=receipt,proto3" json:"receipt,omitempty"`
 	// The dependencies of the resource.
+	// The controller checks for any updates in the dependencies and takes an
+	// action if there has been an update in dependencies from the last time
+	// the action was taken.
+	// Check "refresh" field if it is not possible to list out the dependencies.
+	// Either "dependencies" or "refresh" must be set for the controller to work.
 	Dependencies []*Dependency `protobuf:"bytes,4,rep,name=dependencies,proto3" json:"dependencies,omitempty"`
 	// The action used to generate the resource.
-	// An action can contain references to both the resource and dependencies
-	// Example: "registry compute complexity $dependency0 $resource"
+	// An action can contain references to $resource
+	// Example: "registry compute complexity $resource"
 	Action string `protobuf:"bytes,5,opt,name=action,proto3" json:"action,omitempty"`
+	// Refresh interval in seconds (must be >0).
+	// Generated resource should be regenerated after the specified duration.
+	// This field should be used when the dependencies of the generated resources
+	// are dynamic and cannot be listed out in the manifest.
+	// If "refresh" is set, the dependencies are ignored and the
+	// controller will use this field to decide when to take the action.
+	// If refresh=Ns, the controller will wait for at least N seconds
+	// before trying to regenerate the generated resource.
+	// Either "refresh" or "dependencies" must be set for the controller to work.
+	Refresh *durationpb.Duration `protobuf:"bytes,6,opt,name=refresh,proto3" json:"refresh,omitempty"`
 }
 
 func (x *GeneratedResource) Reset() {
@@ -220,6 +236,13 @@ func (x *GeneratedResource) GetAction() string {
 	return ""
 }
 
+func (x *GeneratedResource) GetRefresh() *durationpb.Duration {
+	if x != nil {
+		return x.Refresh
+	}
+	return nil
+}
+
 // A dependency of a generated resource is another resource in the registry
 // which should always be older than the generated resource. When dependencies
 // are updated, the generated resource that depends on them should be
@@ -297,6 +320,8 @@ var file_google_cloud_apigeeregistry_v1_controller_manifest_proto_rawDesc = []by
 	0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x2e, 0x76, 0x31, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72,
 	0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x61, 0x70,
 	0x69, 0x2f, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x5f, 0x62, 0x65, 0x68, 0x61, 0x76, 0x69, 0x6f, 0x72,
+	0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70,
+	0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x64, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e,
 	0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xe7, 0x01, 0x0a, 0x08, 0x4d, 0x61, 0x6e, 0x69, 0x66,
 	0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52,
 	0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6b, 0x69, 0x6e, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28,
@@ -312,7 +337,7 @@ var file_google_cloud_apigeeregistry_v1_controller_manifest_proto_rawDesc = []by
 	0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x47, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x64, 0x52,
 	0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x42, 0x03, 0xe0, 0x41, 0x02, 0x52, 0x12, 0x67, 0x65,
 	0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73,
-	0x22, 0xdc, 0x01, 0x0a, 0x11, 0x47, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x64, 0x52, 0x65,
+	0x22, 0x91, 0x02, 0x0a, 0x11, 0x47, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x64, 0x52, 0x65,
 	0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x1d, 0x0a, 0x07, 0x70, 0x61, 0x74, 0x74, 0x65, 0x72,
 	0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x03, 0xe0, 0x41, 0x02, 0x52, 0x07, 0x70, 0x61,
 	0x74, 0x74, 0x65, 0x72, 0x6e, 0x12, 0x16, 0x0a, 0x06, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x18,
@@ -325,19 +350,23 @@ var file_google_cloud_apigeeregistry_v1_controller_manifest_proto_rawDesc = []by
 	0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x44, 0x65, 0x70, 0x65, 0x6e, 0x64,
 	0x65, 0x6e, 0x63, 0x79, 0x52, 0x0c, 0x64, 0x65, 0x70, 0x65, 0x6e, 0x64, 0x65, 0x6e, 0x63, 0x69,
 	0x65, 0x73, 0x12, 0x1b, 0x0a, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x05, 0x20, 0x01,
-	0x28, 0x09, 0x42, 0x03, 0xe0, 0x41, 0x02, 0x52, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x22,
-	0x43, 0x0a, 0x0a, 0x44, 0x65, 0x70, 0x65, 0x6e, 0x64, 0x65, 0x6e, 0x63, 0x79, 0x12, 0x1d, 0x0a,
-	0x07, 0x70, 0x61, 0x74, 0x74, 0x65, 0x72, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x03,
-	0xe0, 0x41, 0x02, 0x52, 0x07, 0x70, 0x61, 0x74, 0x74, 0x65, 0x72, 0x6e, 0x12, 0x16, 0x0a, 0x06,
-	0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x66, 0x69,
-	0x6c, 0x74, 0x65, 0x72, 0x42, 0x6e, 0x0a, 0x2d, 0x63, 0x6f, 0x6d, 0x2e, 0x67, 0x6f, 0x6f, 0x67,
-	0x6c, 0x65, 0x2e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2e, 0x61, 0x70, 0x69, 0x67, 0x65, 0x65, 0x72,
-	0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x2e, 0x76, 0x31, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72,
-	0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x42, 0x17, 0x43, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65,
-	0x72, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01,
-	0x5a, 0x22, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x61, 0x70, 0x69,
-	0x67, 0x65, 0x65, 0x2f, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x2f, 0x72, 0x70, 0x63,
-	0x3b, 0x72, 0x70, 0x63, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
+	0x28, 0x09, 0x42, 0x03, 0xe0, 0x41, 0x02, 0x52, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12,
+	0x33, 0x0a, 0x07, 0x72, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b,
+	0x32, 0x19, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62,
+	0x75, 0x66, 0x2e, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x07, 0x72, 0x65, 0x66,
+	0x72, 0x65, 0x73, 0x68, 0x22, 0x43, 0x0a, 0x0a, 0x44, 0x65, 0x70, 0x65, 0x6e, 0x64, 0x65, 0x6e,
+	0x63, 0x79, 0x12, 0x1d, 0x0a, 0x07, 0x70, 0x61, 0x74, 0x74, 0x65, 0x72, 0x6e, 0x18, 0x01, 0x20,
+	0x01, 0x28, 0x09, 0x42, 0x03, 0xe0, 0x41, 0x02, 0x52, 0x07, 0x70, 0x61, 0x74, 0x74, 0x65, 0x72,
+	0x6e, 0x12, 0x16, 0x0a, 0x06, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28,
+	0x09, 0x52, 0x06, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x42, 0x6e, 0x0a, 0x2d, 0x63, 0x6f, 0x6d,
+	0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2e, 0x61, 0x70,
+	0x69, 0x67, 0x65, 0x65, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x2e, 0x76, 0x31, 0x2e,
+	0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x42, 0x17, 0x43, 0x6f, 0x6e, 0x74,
+	0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x50, 0x72,
+	0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x22, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f,
+	0x6d, 0x2f, 0x61, 0x70, 0x69, 0x67, 0x65, 0x65, 0x2f, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72,
+	0x79, 0x2f, 0x72, 0x70, 0x63, 0x3b, 0x72, 0x70, 0x63, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f,
+	0x33,
 }
 
 var (
@@ -354,18 +383,20 @@ func file_google_cloud_apigeeregistry_v1_controller_manifest_proto_rawDescGZIP()
 
 var file_google_cloud_apigeeregistry_v1_controller_manifest_proto_msgTypes = make([]protoimpl.MessageInfo, 3)
 var file_google_cloud_apigeeregistry_v1_controller_manifest_proto_goTypes = []interface{}{
-	(*Manifest)(nil),          // 0: google.cloud.apigeeregistry.v1.controller.Manifest
-	(*GeneratedResource)(nil), // 1: google.cloud.apigeeregistry.v1.controller.GeneratedResource
-	(*Dependency)(nil),        // 2: google.cloud.apigeeregistry.v1.controller.Dependency
+	(*Manifest)(nil),            // 0: google.cloud.apigeeregistry.v1.controller.Manifest
+	(*GeneratedResource)(nil),   // 1: google.cloud.apigeeregistry.v1.controller.GeneratedResource
+	(*Dependency)(nil),          // 2: google.cloud.apigeeregistry.v1.controller.Dependency
+	(*durationpb.Duration)(nil), // 3: google.protobuf.Duration
 }
 var file_google_cloud_apigeeregistry_v1_controller_manifest_proto_depIdxs = []int32{
 	1, // 0: google.cloud.apigeeregistry.v1.controller.Manifest.generated_resources:type_name -> google.cloud.apigeeregistry.v1.controller.GeneratedResource
 	2, // 1: google.cloud.apigeeregistry.v1.controller.GeneratedResource.dependencies:type_name -> google.cloud.apigeeregistry.v1.controller.Dependency
-	2, // [2:2] is the sub-list for method output_type
-	2, // [2:2] is the sub-list for method input_type
-	2, // [2:2] is the sub-list for extension type_name
-	2, // [2:2] is the sub-list for extension extendee
-	0, // [0:2] is the sub-list for field type_name
+	3, // 2: google.cloud.apigeeregistry.v1.controller.GeneratedResource.refresh:type_name -> google.protobuf.Duration
+	3, // [3:3] is the sub-list for method output_type
+	3, // [3:3] is the sub-list for method input_type
+	3, // [3:3] is the sub-list for extension type_name
+	3, // [3:3] is the sub-list for extension extendee
+	0, // [0:3] is the sub-list for field type_name
 }
 
 func init() { file_google_cloud_apigeeregistry_v1_controller_manifest_proto_init() }


Test patch:
diff --git a/cmd/registry/cmd/apply/testdata/sample/artifacts/manifest.yaml b/cmd/registry/cmd/apply/testdata/sample/artifacts/manifest.yaml
index 83bd420..e515149 100644
--- a/cmd/registry/cmd/apply/testdata/sample/artifacts/manifest.yaml
+++ b/cmd/registry/cmd/apply/testdata/sample/artifacts/manifest.yaml
@@ -13,6 +13,7 @@ data:
         - pattern: $resource.spec
           filter: mime_type.contains('openapi')
       action: registry compute lint $resource.spec --linter spectral
+      refresh: null
     - pattern: apis/-/versions/-/specs/-/artifacts/lintstats-spectral
       filter: ""
       receipt: false
@@ -22,6 +23,7 @@ data:
         - pattern: $resource.spec/artifacts/complexity
           filter: ""
       action: registry compute lintstats $resource.spec --linter spectral
+      refresh: null
     - pattern: apis/-/versions/-/specs/-/artifacts/vocabulary
       filter: ""
       receipt: false
@@ -29,6 +31,7 @@ data:
         - pattern: $resource.spec
           filter: ""
       action: registry compute vocabulary $resource.spec
+      refresh: null
     - pattern: apis/-/versions/-/specs/-/artifacts/complexity
       filter: ""
       receipt: false
@@ -36,3 +39,4 @@ data:
         - pattern: $resource.spec
           filter: ""
       action: registry compute complexity $resource.spec
+      refresh: null
diff --git a/cmd/registry/cmd/upload/artifact_test.go b/cmd/registry/cmd/upload/artifact_test.go
index 1b73819..574b813 100644
--- a/cmd/registry/cmd/upload/artifact_test.go
+++ b/cmd/registry/cmd/upload/artifact_test.go
@@ -221,7 +221,7 @@ func TestScoreDefinitionArtifactUpload(t *testing.T) {
 						Artifact: &rpc.ResourcePattern{
 							Pattern: "$resource.spec/artifacts/conformance-apihub-styleguide",
 						},
-						ScoreExpression: "sum(guidelineReportGroups[2].guidelineReports.map(r, has(r.ruleReportGroups[1].ruleReports) ? size(r.ruleReportGroups[1].ruleReports) : 0))",
+						ScoreExpression: "has(guidelineReportGroups[2].guidelineReports) ? sum(guidelineReportGroups[2].guidelineReports.map(r, has(r.ruleReportGroups[1].ruleReports) ? size(r.ruleReportGroups[1].ruleReports) : 0)) : 0",
 					},
 				},
 				Type: &rpc.ScoreDefinition_Integer{
diff --git a/cmd/registry/cmd/upload/testdata/score-definition.yaml b/cmd/registry/cmd/upload/testdata/score-definition.yaml
index 0fe8928..14427ea 100644
--- a/cmd/registry/cmd/upload/testdata/score-definition.yaml
+++ b/cmd/registry/cmd/upload/testdata/score-definition.yaml
@@ -20,7 +20,7 @@ score_formula:
   artifact:
     pattern: "$resource.spec/artifacts/conformance-apihub-styleguide"
   # number of errors from conformance report
-  score_expression: "sum(guidelineReportGroups[2].guidelineReports.map(r, has(r.ruleReportGroups[1].ruleReports) ? size(r.ruleReportGroups[1].ruleReports) : 0))"
+  score_expression: "has(guidelineReportGroups[2].guidelineReports) ? sum(guidelineReportGroups[2].guidelineReports.map(r, has(r.ruleReportGroups[1].ruleReports) ? size(r.ruleReportGroups[1].ruleReports) : 0)) : 0"
 integer: 
   min_value: 0
   max_value: 100
diff --git a/cmd/registry/controller/controller_test.go b/cmd/registry/controller/controller_test.go
index 18eaf9e..5b05260 100644
--- a/cmd/registry/controller/controller_test.go
+++ b/cmd/registry/controller/controller_test.go
@@ -18,6 +18,7 @@ import (
 	"context"
 	"fmt"
 	"testing"
+	"time"
 
 	"github.com/apigee/registry/cmd/registry/core"
 	"github.com/apigee/registry/connection"
@@ -28,6 +29,7 @@ import (
 	"google.golang.org/grpc/codes"
 	"google.golang.org/grpc/status"
 	"google.golang.org/protobuf/proto"
+	"google.golang.org/protobuf/types/known/durationpb"
 )
 
 const gzipOpenAPIv3 = "application/x.openapi+gzip;version=3.0.0"
@@ -959,3 +961,159 @@ func TestMultipleEntitiesArtifacts(t *testing.T) {
 		})
 	}
 }
+
+func TestRefreshArtifacts(t *testing.T) {
+	tests := []struct {
+		desc string
+		seed []seeder.RegistryResource
+		want []*Action
+		wait time.Duration
+	}{
+		{
+			desc: "non-existing artifacts",
+			seed: []seeder.RegistryResource{
+				&rpc.ApiSpec{
+					Name: "projects/controller-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml",
+				},
+				&rpc.ApiSpec{
+					Name: "projects/controller-test/locations/global/apis/petstore/versions/1.0.1/specs/openapi.yaml",
+				},
+				&rpc.ApiSpec{
+					Name: "projects/controller-test/locations/global/apis/petstore/versions/1.1.0/specs/openapi.yaml",
+				},
+			},
+			want: []*Action{
+				{
+					Command:           "registry compute score projects/controller-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml",
+					GeneratedResource: "projects/controller-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/score-receipt",
+					RequiresReceipt:   true,
+				},
+				{
+					Command:           "registry compute score projects/controller-test/locations/global/apis/petstore/versions/1.0.1/specs/openapi.yaml",
+					GeneratedResource: "projects/controller-test/locations/global/apis/petstore/versions/1.0.1/specs/openapi.yaml/artifacts/score-receipt",
+					RequiresReceipt:   true,
+				},
+				{
+					Command:           "registry compute score projects/controller-test/locations/global/apis/petstore/versions/1.1.0/specs/openapi.yaml",
+					GeneratedResource: "projects/controller-test/locations/global/apis/petstore/versions/1.1.0/specs/openapi.yaml/artifacts/score-receipt",
+					RequiresReceipt:   true,
+				},
+			},
+		},
+		{
+			desc: "existing valid artifacts",
+			seed: []seeder.RegistryResource{
+				&rpc.Artifact{
+					Name: "projects/controller-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/score-receipt",
+				},
+				&rpc.Artifact{
+					Name: "projects/controller-test/locations/global/apis/petstore/versions/1.0.1/specs/openapi.yaml/artifacts/score-receipt",
+				},
+				&rpc.Artifact{
+					Name: "projects/controller-test/locations/global/apis/petstore/versions/1.1.0/specs/openapi.yaml/artifacts/score-receipt",
+				},
+			},
+			want: nil,
+		},
+		{
+			desc: "existing invalid artifacts",
+			seed: []seeder.RegistryResource{
+				&rpc.Artifact{
+					Name: "projects/controller-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/score-receipt",
+				},
+				&rpc.Artifact{
+					Name: "projects/controller-test/locations/global/apis/petstore/versions/1.0.1/specs/openapi.yaml/artifacts/score-receipt",
+				},
+				&rpc.Artifact{
+					Name: "projects/controller-test/locations/global/apis/petstore/versions/1.1.0/specs/openapi.yaml/artifacts/score-receipt",
+				},
+			},
+			want: []*Action{
+				{
+					Command:           "registry compute score projects/controller-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml",
+					GeneratedResource: "projects/controller-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/score-receipt",
+					RequiresReceipt:   true,
+				},
+				{
+					Command:           "registry compute score projects/controller-test/locations/global/apis/petstore/versions/1.0.1/specs/openapi.yaml",
+					GeneratedResource: "projects/controller-test/locations/global/apis/petstore/versions/1.0.1/specs/openapi.yaml/artifacts/score-receipt",
+					RequiresReceipt:   true,
+				},
+				{
+					Command:           "registry compute score projects/controller-test/locations/global/apis/petstore/versions/1.1.0/specs/openapi.yaml",
+					GeneratedResource: "projects/controller-test/locations/global/apis/petstore/versions/1.1.0/specs/openapi.yaml/artifacts/score-receipt",
+					RequiresReceipt:   true,
+				},
+			},
+			wait: 5,
+		},
+		{
+			desc: "existing valid artifacts",
+			seed: []seeder.RegistryResource{
+				&rpc.Artifact{
+					Name: "projects/controller-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/score-receipt",
+				},
+				&rpc.Artifact{
+					Name: "projects/controller-test/locations/global/apis/petstore/versions/1.0.1/specs/openapi.yaml/artifacts/score-receipt",
+				},
+				&rpc.Artifact{
+					Name: "projects/controller-test/locations/global/apis/petstore/versions/1.1.0/specs/openapi.yaml/artifacts/score-receipt",
+				},
+			},
+			want: nil,
+		},
+	}
+
+	const projectID = "controller-test"
+	for _, test := range tests {
+		t.Run(test.desc, func(t *testing.T) {
+			ctx := context.Background()
+			registryClient, err := connection.NewClient(ctx)
+			if err != nil {
+				t.Fatalf("Failed to create client: %+v", err)
+			}
+			t.Cleanup(func() { registryClient.Close() })
+
+			adminClient, err := connection.NewAdminClient(ctx)
+			if err != nil {
+				t.Fatalf("Failed to create client: %+v", err)
+			}
+			t.Cleanup(func() { adminClient.Close() })
+
+			deleteProject(ctx, adminClient, t, "controller-test")
+			t.Cleanup(func() { deleteProject(ctx, adminClient, t, "controller-test") })
+
+			client := seeder.Client{
+				RegistryClient: registryClient,
+				AdminClient:    adminClient,
+			}
+
+			if err := seeder.SeedRegistry(ctx, client, test.seed...); err != nil {
+				t.Fatalf("Setup: failed to seed registry: %s", err)
+			}
+
+			time.Sleep(test.wait * time.Second)
+
+			manifest := &rpc.Manifest{
+				Id: "controller-test",
+				GeneratedResources: []*rpc.GeneratedResource{
+					{
+						Pattern: "apis/-/versions/-/specs/-/artifacts/score-receipt",
+						Receipt: true,
+						Refresh: &durationpb.Duration{
+							Seconds: 2,
+						},
+						Action: "registry compute score $resource.spec",
+					},
+				},
+			}
+			actions := ProcessManifest(ctx, registryClient, projectID, manifest)
+
+			if diff := cmp.Diff(test.want, actions, sortActions); diff != "" {
+				t.Errorf("ProcessManifest(%+v) returned unexpected diff (-want +got):\n%s", manifest, diff)
+			}
+
+			deleteProject(ctx, adminClient, t, "controller-test")
+		})
+	}
+}
diff --git a/cmd/registry/controller/parser_test.go b/cmd/registry/controller/parser_test.go
index f45a473..b0ef663 100644
--- a/cmd/registry/controller/parser_test.go
+++ b/cmd/registry/controller/parser_test.go
@@ -19,6 +19,7 @@ import (
 	"testing"
 
 	"github.com/apigee/registry/rpc"
+	"google.golang.org/protobuf/types/known/durationpb"
 )
 
 func TestGenerateCommand(t *testing.T) {
@@ -173,6 +174,28 @@ func TestValidateGeneratedResourceEntry(t *testing.T) {
 				Action: "registry generate summary $resource.version",
 			},
 		},
+		{
+			desc: "refresh field with missing dependencies",
+			generatedResource: &rpc.GeneratedResource{
+				Pattern: "apis/-/versions/-/artifacts/score-receipt",
+				Receipt: true,
+				Refresh: &durationpb.Duration{
+					Seconds: 5,
+				},
+				Action: "registry generate summary $resource.version",
+			},
+		},
+		{
+			desc: "refresh field (in nanoseconds)",
+			generatedResource: &rpc.GeneratedResource{
+				Pattern: "apis/-/versions/-/artifacts/score-receipt",
+				Receipt: true,
+				Refresh: &durationpb.Duration{
+					Nanos: 5,
+				},
+				Action: "registry generate summary $resource.version",
+			},
+		},
 	}
 	for _, test := range tests {
 		t.Run(test.desc, func(t *testing.T) {
@@ -247,6 +270,25 @@ func TestValidateGeneratedResourceEntryError(t *testing.T) {
 				Action: "registry generate summary $resource.spec", // Correct pattern: registry generate summary $resource.version
 			},
 		},
+		{
+			desc: "refresh field equal to 0",
+			generatedResource: &rpc.GeneratedResource{
+				Pattern: "apis/-/versions/-/artifacts/score-receipt",
+				Receipt: true,
+				Refresh: &durationpb.Duration{
+					Seconds: 0,
+				},
+				Action: "registry generate summary $resource.version",
+			},
+		},
+		{
+			desc: "missing dependencies and refresh",
+			generatedResource: &rpc.GeneratedResource{
+				Pattern: "apis/-/versions/-/artifacts/score-receipt",
+				Receipt: true,
+				Action:  "registry generate summary $resource.version",
+			},
+		},
 	}
 	for _, test := range tests {
 		t.Run(test.desc, func(t *testing.T) {
diff --git a/cmd/registry/controller/testdata/manifest.yaml b/cmd/registry/controller/testdata/manifest.yaml
index f34e9cf..ae407e3 100644
--- a/cmd/registry/controller/testdata/manifest.yaml
+++ b/cmd/registry/controller/testdata/manifest.yaml
@@ -14,31 +14,29 @@
 
 id: "test-manifest"
 generated_resources:
-  - pattern: apis/-/versions/-/specs/-/artifacts/lint-gnostic
+  # - pattern: apis/-/versions/-/specs/-/artifacts/score-receipt
+  #   receipt: true
+  #   action: "registry compute score $resource.spec"
+  #   ttl: 5s
+  - pattern: apis/-/versions/-/specs/-/artifacts/complexity
     dependencies:
       - pattern: $resource.spec
-        filter: "mime_type.contains('openapi')"
-    action: "registry compute lint $resource.spec --linter gnostic"
-  - pattern: apis/-/versions/-/specs/-/artifacts/lint-aip
-    dependencies:
-      - pattern: $resource.spec
-        filter: "mime_type.contains('protobuf')"
-    action: "registry compute lint $resource.spec --linter aip"
-  - pattern: apis/-/versions/-/specs/-/artifacts/lintstats-gnostic
-    dependencies:
-      - pattern: $resource.spec/artifacts/lint-gnostic
-      - pattern: $resource.spec/artifacts/complexity
-    action: "registry compute lintstats $resource.spec --linter gnostic"
-  - pattern: apis/-/artifacts/vocabulary
-    dependencies:
-      - pattern: $resource.api/versions/-/specs/-
-    action: "registry compute vocabulary $resource.api"
-  - pattern: apis/-/versions/-/artifacts/vocabulary
-    dependencies:
-      - pattern: $resource.version/-/specs/-
-    action: "registry compute vocabulary $resource.version"
-  - pattern: apis/-/versions/-/specs/-/artifacts/score
-    dependencies:
-      - pattern: $resource.spec/artifacts/lint-gnostic
-      - pattern: $resource.spec/artifacts/complexity
-    action: "registry compute score $resource.spec/artifacts/lint-gnostic $resource.spec/artifacts/complexity"
+    action: "registry compute complexity $resource.spec"
+  # - pattern: apis/-/versions/-/specs/-/artifacts/lintstats-gnostic
+  #   dependencies:
+  #     - pattern: $resource.spec/artifacts/lint-gnostic
+  #     - pattern: $resource.spec/artifacts/complexity
+  #   action: "registry compute lintstats $resource.spec --linter gnostic"
+  # - pattern: apis/-/artifacts/vocabulary
+  #   dependencies:
+  #     - pattern: $resource.api/versions/-/specs/-
+  #   action: "registry compute vocabulary $resource.api"
+  # - pattern: apis/-/versions/-/artifacts/vocabulary
+  #   dependencies:
+  #     - pattern: $resource.version/-/specs/-
+  #   action: "registry compute vocabulary $resource.version"
+  # - pattern: apis/-/versions/-/specs/-/artifacts/score
+  #   dependencies:
+  #     - pattern: $resource.spec/artifacts/lint-gnostic
+  #     - pattern: $resource.spec/artifacts/complexity
+  #   action: "registry compute score $resource.spec/artifacts/lint-gnostic $resource.spec/artifacts/complexity"