Repository: GoogleContainerTools/kpt
Pull request #3632 (2022-10-20 22:40:32)

patch porch cli to use new workspace field
Builds on https://github.com/GoogleContainerTools/kpt/pull/3593 (first four commits here are the same), so only the last commit needs review. 

natasha41575: @droot @mortent This PR is ready for review, PTAL when you can. It builds off of https://github.com/GoogleContainerTools/kpt/pull/3593, so you only need to review the last commit here. 

Patch:
diff --git a/commands/alpha/rpkg/clone/command.go b/commands/alpha/rpkg/clone/command.go
index 536890ab..b35d8dd0 100644
--- a/commands/alpha/rpkg/clone/command.go
+++ b/commands/alpha/rpkg/clone/command.go
@@ -68,7 +68,7 @@ func newRunner(ctx context.Context, rcg *genericclioptions.ConfigFlags) *runner
 	c.Flags().StringVar(&r.directory, "directory", "", "Directory within the repository where the upstream package is located.")
 	c.Flags().StringVar(&r.ref, "ref", "", "Branch in the repository where the upstream package is located.")
 	c.Flags().StringVar(&r.repository, "repository", "", "Repository to which package will be cloned (downstream repository).")
-	c.Flags().StringVar(&r.revision, "revision", "v1", "Revision of the downstream package.")
+	c.Flags().StringVar(&r.workspace, "workspace", "v1", "WorkspaceName of the downstream package.")
 
 	return r
 }
@@ -86,7 +86,7 @@ type runner struct {
 	directory  string
 	ref        string
 	repository string // Target repository
-	revision   string // Target package revision
+	workspace  string // Target workspaceName
 	target     string // Target package name
 }
 
@@ -112,6 +112,10 @@ func (r *runner) preRunE(cmd *cobra.Command, args []string) error {
 		return errors.E(op, fmt.Errorf("--repository is required to specify downstream repository"))
 	}
 
+	if r.workspace == "" {
+		return errors.E(op, fmt.Errorf("--workspace is required to specify downstream workspaceName"))
+	}
+
 	source := args[0]
 	target := args[1]
 
@@ -192,7 +196,7 @@ func (r *runner) runE(cmd *cobra.Command, args []string) error {
 		},
 		Spec: porchapi.PackageRevisionSpec{
 			PackageName:    r.target,
-			Revision:       r.revision,
+			WorkspaceName:  porchapi.WorkspaceName(r.workspace),
 			RepositoryName: r.repository,
 			Tasks: []porchapi.Task{
 				{
diff --git a/commands/alpha/rpkg/copy/command.go b/commands/alpha/rpkg/copy/command.go
index 82a80f07..929aaea3 100644
--- a/commands/alpha/rpkg/copy/command.go
+++ b/commands/alpha/rpkg/copy/command.go
@@ -17,15 +17,12 @@ package copy
 import (
 	"context"
 	"fmt"
-	"strconv"
-	"strings"
 
 	"github.com/GoogleContainerTools/kpt/internal/docs/generated/rpkgdocs"
 	"github.com/GoogleContainerTools/kpt/internal/errors"
 	"github.com/GoogleContainerTools/kpt/internal/util/porch"
 	porchapi "github.com/GoogleContainerTools/kpt/porch/api/porch/v1alpha1"
 	"github.com/spf13/cobra"
-	"golang.org/x/mod/semver"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/types"
 	"k8s.io/cli-runtime/pkg/genericclioptions"
@@ -55,7 +52,7 @@ func newRunner(ctx context.Context, rcg *genericclioptions.ConfigFlags) *runner
 		RunE:    r.runE,
 		Hidden:  porch.HidePorchCommands,
 	}
-	r.Command.Flags().StringVar(&r.revision, "revision", "", "Revision of the copied package.")
+	r.Command.Flags().StringVar(&r.workspace, "workspace", "", "WorkspaceName of the copy of the package.")
 	return r
 }
 
@@ -67,7 +64,7 @@ type runner struct {
 
 	copy porchapi.PackageEditTaskSpec
 
-	revision string // Target package revision
+	workspace string // Target package revision workspaceName
 }
 
 func (r *runner) preRunE(cmd *cobra.Command, args []string) error {
@@ -126,79 +123,15 @@ func (r *runner) getPackageRevisionSpec() (*porchapi.PackageRevisionSpec, error)
 		return nil, err
 	}
 
-	if r.revision == "" {
-		var err error
-		r.revision, err = r.defaultPackageRevision(
-			packageRevision.Spec.PackageName,
-			packageRevision.Spec.RepositoryName,
-		)
-		if err != nil {
-			return nil, err
-		}
+	if r.workspace == "" {
+		return nil, fmt.Errorf("--workspace is required to specify downstream workspaceName")
 	}
 
 	spec := &porchapi.PackageRevisionSpec{
 		PackageName:    packageRevision.Spec.PackageName,
-		Revision:       r.revision,
+		WorkspaceName:  porchapi.WorkspaceName(r.workspace),
 		RepositoryName: packageRevision.Spec.RepositoryName,
 	}
 	spec.Tasks = packageRevision.Spec.Tasks
 	return spec, nil
 }
-
-// defaultPackageRevision attempts to return a default package revision number
-// of "latest + 1" given a package name, repository, and namespace.
-func (r *runner) defaultPackageRevision(packageName, repository string) (string, error) {
-	// get all package revisions
-	packageRevisionList := porchapi.PackageRevisionList{}
-	err := r.client.List(r.ctx, &packageRevisionList, client.InNamespace(*r.cfg.Namespace))
-	if err != nil {
-		return "", err
-	}
-
-	var latestRevision string
-	allRevisions := make(map[string]bool) // this is a map for quick access
-
-	for _, rev := range packageRevisionList.Items {
-		if packageName != rev.Spec.PackageName ||
-			repository != rev.Spec.RepositoryName {
-			continue
-		}
-
-		if latest, ok := rev.Labels[porchapi.LatestPackageRevisionKey]; ok {
-			if latest == porchapi.LatestPackageRevisionValue {
-				latestRevision = rev.Spec.Revision
-			}
-		}
-		allRevisions[rev.Spec.Revision] = true
-	}
-	if latestRevision == "" {
-		return "", fmt.Errorf("no published packages exist; explicit --revision flag is required")
-	}
-
-	next, err := nextRevisionNumber(latestRevision)
-	if err != nil {
-		return "", err
-	}
-	if _, ok := allRevisions[next]; ok {
-		return "", fmt.Errorf("default revision %q already exists; explicit --revision flag is required", next)
-	}
-	return next, err
-}
-
-func nextRevisionNumber(latestRevision string) (string, error) {
-	if !semver.IsValid(latestRevision) {
-		return "", fmt.Errorf("invalid revision format %s; explicit --revision flag is required", latestRevision)
-	}
-
-	parts := strings.Split(latestRevision[1:], ".")
-	lastIndex := len(parts) - 1
-	i, err := strconv.Atoi(parts[lastIndex])
-	if err != nil {
-		return "", err
-	}
-	i++
-	parts[lastIndex] = strconv.Itoa(i)
-	next := "v" + strings.Join(parts, ".")
-	return next, nil
-}
diff --git a/commands/alpha/rpkg/get/command.go b/commands/alpha/rpkg/get/command.go
index d2a843b4..80149968 100644
--- a/commands/alpha/rpkg/get/command.go
+++ b/commands/alpha/rpkg/get/command.go
@@ -62,6 +62,8 @@ func newRunner(ctx context.Context, rcg *genericclioptions.ConfigFlags) *runner
 	// Create flags
 	cmd.Flags().StringVar(&r.packageName, "name", "", "Name of the packages to get. Any package whose name contains this value will be included in the results.")
 	cmd.Flags().StringVar(&r.revision, "revision", "", "Revision of the packages to get. Any package whose revision matches this value will be included in the results.")
+	cmd.Flags().StringVar(&r.workspace, "workspace", "",
+		"WorkspaceName of the packages to get. Any package whose workspaceName matches this value will be included in the results.")
 
 	r.getFlags.AddFlags(cmd)
 	r.printFlags.AddFlags(cmd)
@@ -80,6 +82,7 @@ type runner struct {
 	// Flags
 	packageName string
 	revision    string
+	workspace   string
 	printFlags  *get.PrintFlags
 
 	requestTable bool
@@ -136,6 +139,9 @@ func (r *runner) runE(cmd *cobra.Command, args []string) error {
 		if r.revision != "" {
 			fieldSelector = fields.OneTermEqualSelector("spec.revision", r.revision)
 		}
+		if r.workspace != "" {
+			fieldSelector = fields.OneTermEqualSelector("spec.workspaceName", r.workspace)
+		}
 		if r.packageName != "" {
 			fieldSelector = fields.OneTermEqualSelector("spec.packageName", r.packageName)
 		}
@@ -234,12 +240,19 @@ func (r *runner) packageRevisionMatches(o *unstructured.Unstructured) (bool, err
 	if err != nil {
 		return false, err
 	}
+	workspace, _, err := unstructured.NestedString(o.Object, "spec", "workspaceName")
+	if err != nil {
+		return false, err
+	}
 	if r.packageName != "" && r.packageName != packageName {
 		return false, nil
 	}
 	if r.revision != "" && r.revision != revision {
 		return false, nil
 	}
+	if r.workspace != "" && r.workspace != workspace {
+		return false, nil
+	}
 	return true, nil
 }
 
@@ -264,6 +277,7 @@ func (r *runner) filterTableRows(table *metav1.Table) error {
 	filtered := make([]metav1.TableRow, 0, len(table.Rows))
 	packageNameCol := findColumn(table.ColumnDefinitions, "Package")
 	revisionCol := findColumn(table.ColumnDefinitions, "Revision")
+	workspaceCol := findColumn(table.ColumnDefinitions, "WorkspaceName")
 
 	for i := range table.Rows {
 		row := &table.Rows[i]
@@ -278,6 +292,11 @@ func (r *runner) filterTableRows(table *metav1.Table) error {
 				continue
 			}
 		}
+		if workspace, ok := getStringCell(row.Cells, workspaceCol); ok {
+			if r.workspace != "" && r.workspace != workspace {
+				continue
+			}
+		}
 
 		// Row matches
 		filtered = append(filtered, *row)
diff --git a/commands/alpha/rpkg/init/command.go b/commands/alpha/rpkg/init/command.go
index 5c524e39..442e4f79 100644
--- a/commands/alpha/rpkg/init/command.go
+++ b/commands/alpha/rpkg/init/command.go
@@ -57,7 +57,7 @@ func newRunner(ctx context.Context, rcg *genericclioptions.ConfigFlags) *runner
 	c.Flags().StringSliceVar(&r.Keywords, "keywords", []string{}, "list of keywords for the package.")
 	c.Flags().StringVar(&r.Site, "site", "", "link to page with information about the package.")
 	c.Flags().StringVar(&r.repository, "repository", "", "Repository to which package will be cloned (downstream repository).")
-	c.Flags().StringVar(&r.revision, "revision", "v1", "Revision of the downstream package.")
+	c.Flags().StringVar(&r.workspace, "workspace", "", "WorkspaceName of the downstream package.")
 
 	return r
 }
@@ -74,7 +74,7 @@ type runner struct {
 	Site        string
 	name        string // Target package name
 	repository  string // Target repository
-	revision    string // Target revision
+	workspace   string // Target workspace name
 }
 
 func (r *runner) preRunE(cmd *cobra.Command, args []string) error {
@@ -115,7 +115,7 @@ func (r *runner) runE(cmd *cobra.Command, args []string) error {
 		},
 		Spec: porchapi.PackageRevisionSpec{
 			PackageName:    r.name,
-			Revision:       r.revision,
+			WorkspaceName:  porchapi.WorkspaceName(r.workspace),
 			RepositoryName: r.repository,
 			Tasks: []porchapi.Task{
 				{
diff --git a/commands/alpha/rpkg/update/discover.go b/commands/alpha/rpkg/update/discover.go
index 2a4734d8..c8cc4577 100644
--- a/commands/alpha/rpkg/update/discover.go
+++ b/commands/alpha/rpkg/update/discover.go
@@ -64,7 +64,7 @@ func (r *runner) discoverUpdates(cmd *cobra.Command, args []string) error {
 func (r *runner) findUpstreamUpdates(prs []porchapi.PackageRevision, repositories *configapi.RepositoryList, w io.Writer) error {
 	var upstreamUpdates [][]string
 	for _, pr := range prs {
-		availableUpdates, upstreamName := r.availableUpdates(pr.Status.UpstreamLock, repositories)
+		availableUpdates, upstreamName, _ := r.availableUpdates(pr.Status.UpstreamLock, repositories)
 		if len(availableUpdates) == 0 {
 			upstreamUpdates = append(upstreamUpdates, []string{pr.Name, upstreamName, "No update available"})
 		} else {
@@ -84,32 +84,42 @@ func (r *runner) findDownstreamUpdates(prs []porchapi.PackageRevision, repositor
 	downstreamUpdatesMap := make(map[string][]porchapi.PackageRevision)
 
 	for _, pr := range prs {
-		availableUpdates, _ := r.availableUpdates(pr.Status.UpstreamLock, repositories)
+		availableUpdates, _, draftName := r.availableUpdates(pr.Status.UpstreamLock, repositories)
 		for _, update := range availableUpdates {
-			key := fmt.Sprintf("%s:%s", update.Name, update.Spec.Revision)
+			key := fmt.Sprintf("%s:%s:%s", update.Name, update.Spec.Revision, draftName)
 			downstreamUpdatesMap[key] = append(downstreamUpdatesMap[key], pr)
 		}
 	}
 	return printDownstreamUpdates(downstreamUpdatesMap, args, w)
 }
 
-func (r *runner) availableUpdates(upstreamLock *porchapi.UpstreamLock, repositories *configapi.RepositoryList) ([]porchapi.PackageRevision, string) {
+func (r *runner) availableUpdates(upstreamLock *porchapi.UpstreamLock, repositories *configapi.RepositoryList) ([]porchapi.PackageRevision, string, string) {
 	var availableUpdates []porchapi.PackageRevision
 	var upstream string
 
 	if upstreamLock == nil || upstreamLock.Git == nil {
-		return nil, ""
+		return nil, "", ""
 	}
+	var currentUpstreamRevision string
+	var draftName string
+
 	// separate the revision number from the package name
-	lastIndex := strings.LastIndex(upstreamLock.Git.Ref, "v")
-	if lastIndex < 0 {
-		return nil, ""
+	lastIndex := strings.LastIndex(upstreamLock.Git.Ref, "/")
+
+	if strings.HasPrefix(upstreamLock.Git.Ref, "drafts") {
+		// The upstream is not a published package, so doesn't have a revision number.
+		// Use v0 as a placeholder, so that all published packages get returned as available
+		// updates.
+		currentUpstreamRevision = "v0"
+		draftName = upstreamLock.Git.Ref[lastIndex+1:]
+	} else {
+		currentUpstreamRevision = upstreamLock.Git.Ref[lastIndex+1:]
 	}
-	currentUpstreamRevision := upstreamLock.Git.Ref[lastIndex:]
 
 	// upstream.git.ref could look like drafts/pkgname/version or pkgname/version
-	upstreamPackageName := upstreamLock.Git.Ref[:lastIndex-1]
-	upstreamPackageName = strings.TrimPrefix(upstreamPackageName, "drafts/")
+	upstreamPackageName := upstreamLock.Git.Ref[:lastIndex]
+	upstreamPackageName = strings.TrimPrefix(upstreamPackageName, "drafts")
+	upstreamPackageName = strings.TrimPrefix(upstreamPackageName, "/")
 
 	if !strings.HasSuffix(upstreamLock.Git.Repo, ".git") {
 		upstreamLock.Git.Repo += ".git"
@@ -139,7 +149,7 @@ func (r *runner) availableUpdates(upstreamLock *porchapi.UpstreamLock, repositor
 		}
 	}
 
-	return availableUpdates, upstream
+	return availableUpdates, upstream, draftName
 }
 
 // fetches all registered repositories
@@ -183,7 +193,14 @@ func printDownstreamUpdates(downstreamUpdatesMap map[string][]porchapi.PackageRe
 		split := strings.Split(upstreamPkgRev, ":")
 		upstreamPkgRevName := split[0]
 		upstreamPkgRevNum := split[1]
+		draftName := split[2]
 		for _, downstreamPkgRev := range downstreamPkgRevs {
+			if draftName != "" {
+				// the upstream package revision is not published, so does not have a revision number
+				downstreamUpdates = append(downstreamUpdates,
+					[]string{upstreamPkgRevName, downstreamPkgRev.Name, fmt.Sprintf("(draft %q)->%s", draftName, upstreamPkgRevNum)})
+				continue
+			}
 			// figure out which upstream revision the downstream revision is based on
 			lastIndex := strings.LastIndex(downstreamPkgRev.Status.UpstreamLock.Git.Ref, "v")
 			if lastIndex < 0 {
diff --git a/go.mod b/go.mod
index 81026f0a..c5e57afa 100644
--- a/go.mod
+++ b/go.mod
@@ -3,7 +3,7 @@ module github.com/GoogleContainerTools/kpt
 go 1.18
 
 require (
-	github.com/GoogleContainerTools/kpt/porch/api v0.0.0-20220821193112-4792e5fa18ee
+	github.com/GoogleContainerTools/kpt/porch/api v0.0.0-20221027171429-622c0141f3d7
 	github.com/bytecodealliance/wasmtime-go v0.39.0
 	github.com/cpuguy83/go-md2man/v2 v2.0.2
 	github.com/go-errors/errors v1.4.2
diff --git a/go.sum b/go.sum
index 88f68a23..c3c07a1a 100644
--- a/go.sum
+++ b/go.sum
@@ -85,8 +85,8 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03
 github.com/BurntSushi/toml v0.4.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
 github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
 github.com/Djarvur/go-err113 v0.0.0-20210108212216-aea10b59be24/go.mod h1:4UJr5HIiMZrwgkSPdsjy2uOQExX/WEILpIrO9UPGuXs=
-github.com/GoogleContainerTools/kpt/porch/api v0.0.0-20220821193112-4792e5fa18ee h1:FSW33xqr8SeoZWQ0MgKtjosMmaUBoHa/z0Tlp493528=
-github.com/GoogleContainerTools/kpt/porch/api v0.0.0-20220821193112-4792e5fa18ee/go.mod h1:VUrDv94BkVOEE/MEwwqVr7C1K0sWxF/QsoIZvpQacnA=
+github.com/GoogleContainerTools/kpt/porch/api v0.0.0-20221027171429-622c0141f3d7 h1:WKfyM9TDvHaGJ0n98rO/U7qkPL8wAWPvLKQYNT7TNvY=
+github.com/GoogleContainerTools/kpt/porch/api v0.0.0-20221027171429-622c0141f3d7/go.mod h1:ASrhnLAL4ahTuiUJyepqcpVRXIoRMJyDs8/eSxwhgZM=
 github.com/MakeNowJust/heredoc v0.0.0-20170808103936-bb23615498cd h1:sjQovDkwrZp8u+gxLtPgKGjk5hCxuy2hrRejBTA9xFU=
 github.com/MakeNowJust/heredoc v0.0.0-20170808103936-bb23615498cd/go.mod h1:64YHyfSL2R96J44Nlwm39UHepQbyR5q10x7iYa1ks2E=
 github.com/Masterminds/goutils v1.1.0/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
diff --git a/porch/api/generated/clientset/versioned/fake/register.go b/porch/api/generated/clientset/versioned/fake/register.go
index 36b9da3c..2b1d2df7 100644
--- a/porch/api/generated/clientset/versioned/fake/register.go
+++ b/porch/api/generated/clientset/versioned/fake/register.go
@@ -35,14 +35,14 @@ var localSchemeBuilder = runtime.SchemeBuilder{
 // AddToScheme adds all types of this clientset into the given scheme. This allows composition
 // of clientsets, like in:
 //
-//   import (
-//     "k8s.io/client-go/kubernetes"
-//     clientsetscheme "k8s.io/client-go/kubernetes/scheme"
-//     aggregatorclientsetscheme "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/scheme"
-//   )
+//	import (
+//	  "k8s.io/client-go/kubernetes"
+//	  clientsetscheme "k8s.io/client-go/kubernetes/scheme"
+//	  aggregatorclientsetscheme "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/scheme"
+//	)
 //
-//   kclientset, _ := kubernetes.NewForConfig(c)
-//   _ = aggregatorclientsetscheme.AddToScheme(clientsetscheme.Scheme)
+//	kclientset, _ := kubernetes.NewForConfig(c)
+//	_ = aggregatorclientsetscheme.AddToScheme(clientsetscheme.Scheme)
 //
 // After this, RawExtensions in Kubernetes types will serialize kube-aggregator types
 // correctly.
diff --git a/porch/api/generated/clientset/versioned/scheme/register.go b/porch/api/generated/clientset/versioned/scheme/register.go
index cee00d25..90fc1da2 100644
--- a/porch/api/generated/clientset/versioned/scheme/register.go
+++ b/porch/api/generated/clientset/versioned/scheme/register.go
@@ -35,14 +35,14 @@ var localSchemeBuilder = runtime.SchemeBuilder{
 // AddToScheme adds all types of this clientset into the given scheme. This allows composition
 // of clientsets, like in:
 //
-//   import (
-//     "k8s.io/client-go/kubernetes"
-//     clientsetscheme "k8s.io/client-go/kubernetes/scheme"
-//     aggregatorclientsetscheme "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/scheme"
-//   )
+//	import (
+//	  "k8s.io/client-go/kubernetes"
+//	  clientsetscheme "k8s.io/client-go/kubernetes/scheme"
+//	  aggregatorclientsetscheme "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/scheme"
+//	)
 //
-//   kclientset, _ := kubernetes.NewForConfig(c)
-//   _ = aggregatorclientsetscheme.AddToScheme(clientsetscheme.Scheme)
+//	kclientset, _ := kubernetes.NewForConfig(c)
+//	_ = aggregatorclientsetscheme.AddToScheme(clientsetscheme.Scheme)
 //
 // After this, RawExtensions in Kubernetes types will serialize kube-aggregator types
 // correctly.
diff --git a/porch/api/generated/openapi/zz_generated.openapi.go b/porch/api/generated/openapi/zz_generated.openapi.go
index e96e5d0a..e4639964 100644
--- a/porch/api/generated/openapi/zz_generated.openapi.go
+++ b/porch/api/generated/openapi/zz_generated.openapi.go
@@ -815,7 +815,7 @@ func schema_porch_api_porch_v1alpha1_PackageInitTaskSpec(ref common.ReferenceCal
 					},
 					"description": {
 						SchemaProps: spec.SchemaProps{
-							Description: "`Description` is a short description of the package.",
+							Description: "`WorkspaceName` is a short description of the package.",
 							Type:        []string{"string"},
 							Format:      "",
 						},
@@ -1153,6 +1153,13 @@ func schema_porch_api_porch_v1alpha1_PackageRevisionResourcesSpec(ref common.Ref
 							Format:      "",
 						},
 					},
+					"workspaceName": {
+						SchemaProps: spec.SchemaProps{
+							Description: "WorkspaceName identifies the workspace of the package.",
+							Type:        []string{"string"},
+							Format:      "",
+						},
+					},
 					"revision": {
 						SchemaProps: spec.SchemaProps{
 							Description: "Revision identifies the version of the package.",
@@ -1225,16 +1232,23 @@ func schema_porch_api_porch_v1alpha1_PackageRevisionSpec(ref common.ReferenceCal
 							Format:      "",
 						},
 					},
-					"revision": {
+					"repository": {
 						SchemaProps: spec.SchemaProps{
-							Description: "Revision identifies the version of the package.",
+							Description: "RepositoryName is the name of the Repository object containing this package.",
 							Type:        []string{"string"},
 							Format:      "",
 						},
 					},
-					"repository": {
+					"workspaceName": {
 						SchemaProps: spec.SchemaProps{
-							Description: "RepositoryName is the name of the Repository object containing this package.",
+							Description: "WorkspaceName is a short, unique description of the changes contained in this package revision.",
+							Type:        []string{"string"},
+							Format:      "",
+						},
+					},
+					"revision": {
+						SchemaProps: spec.SchemaProps{
+							Description: "Revision identifies the version of the package.",
 							Type:        []string{"string"},
 							Format:      "",
 						},
@@ -1294,7 +1308,8 @@ func schema_porch_api_porch_v1alpha1_PackageRevisionStatus(ref common.ReferenceC
 				Properties: map[string]spec.Schema{
 					"upstreamLock": {
 						SchemaProps: spec.SchemaProps{
-							Ref: ref("github.com/GoogleContainerTools/kpt/porch/api/porch/v1alpha1.UpstreamLock"),
+							Description: "UpstreamLock identifies the upstream data for this package.",
+							Ref:         ref("github.com/GoogleContainerTools/kpt/porch/api/porch/v1alpha1.UpstreamLock"),
 						},
 					},
 					"publishedBy": {
diff --git a/porch/api/porch/types_packagerevisionresources.go b/porch/api/porch/types_packagerevisionresources.go
index ce2a412c..01486020 100644
--- a/porch/api/porch/types_packagerevisionresources.go
+++ b/porch/api/porch/types_packagerevisionresources.go
@@ -45,6 +45,9 @@ type PackageRevisionResourcesSpec struct {
 	// PackageName identifies the package in the repository.
 	PackageName string `json:"packageName,omitempty"`
 
+	// WorkspaceName identifies the workspace of the package.
+	WorkspaceName WorkspaceName `json:"workspaceName,omitempty"`
+
 	// Revision identifies the version of the package.
 	Revision string `json:"revision,omitempty"`
 
diff --git a/porch/api/porch/types_packagerevisions.go b/porch/api/porch/types_packagerevisions.go
index 146d3d55..8dda59f2 100644
--- a/porch/api/porch/types_packagerevisions.go
+++ b/porch/api/porch/types_packagerevisions.go
@@ -49,17 +49,22 @@ const (
 	PackageRevisionLifecyclePublished PackageRevisionLifecycle = "Published"
 )
 
+type WorkspaceName string
+
 // PackageRevisionSpec defines the desired state of PackageRevision
 type PackageRevisionSpec struct {
 	// PackageName identifies the package in the repository.
 	PackageName string `json:"packageName,omitempty"`
 
-	// Revision identifies the version of the package.
-	Revision string `json:"revision,omitempty"`
-
 	// RepositoryName is the name of the Repository object containing this package.
 	RepositoryName string `json:"repository,omitempty"`
 
+	// WorkspaceName is a short, unique description of the changes contained in this package revision.
+	WorkspaceName WorkspaceName `json:"workspaceName,omitempty"`
+
+	// Revision identifies the version of the package.
+	Revision string `json:"revision,omitempty"`
+
 	// Parent references a package that provides resources to us
 	Parent *ParentReference `json:"parent,omitempty"`
 
@@ -84,6 +89,7 @@ type ParentReference struct {
 
 // PackageRevisionStatus defines the observed state of PackageRevision
 type PackageRevisionStatus struct {
+	// UpstreamLock identifies the upstream data for this package.
 	UpstreamLock *UpstreamLock `json:"upstreamLock,omitempty"`
 
 	// PublishedBy is the identity of the user who approved the packagerevision.
diff --git a/porch/api/porch/v1alpha1/types_packagerevisionresources.go b/porch/api/porch/v1alpha1/types_packagerevisionresources.go
index 86904734..cf286031 100644
--- a/porch/api/porch/v1alpha1/types_packagerevisionresources.go
+++ b/porch/api/porch/v1alpha1/types_packagerevisionresources.go
@@ -45,6 +45,9 @@ type PackageRevisionResourcesSpec struct {
 	// PackageName identifies the package in the repository.
 	PackageName string `json:"packageName,omitempty"`
 
+	// WorkspaceName identifies the workspace of the package.
+	WorkspaceName WorkspaceName `json:"workspaceName,omitempty"`
+
 	// Revision identifies the version of the package.
 	Revision string `json:"revision,omitempty"`
 
diff --git a/porch/api/porch/v1alpha1/types_packagerevisions.go b/porch/api/porch/v1alpha1/types_packagerevisions.go
index e259cd18..caa8a6fe 100644
--- a/porch/api/porch/v1alpha1/types_packagerevisions.go
+++ b/porch/api/porch/v1alpha1/types_packagerevisions.go
@@ -57,17 +57,22 @@ const (
 	PackageRevisionLifecyclePublished PackageRevisionLifecycle = "Published"
 )
 
+type WorkspaceName string
+
 // PackageRevisionSpec defines the desired state of PackageRevision
 type PackageRevisionSpec struct {
 	// PackageName identifies the package in the repository.
 	PackageName string `json:"packageName,omitempty"`
 
-	// Revision identifies the version of the package.
-	Revision string `json:"revision,omitempty"`
-
 	// RepositoryName is the name of the Repository object containing this package.
 	RepositoryName string `json:"repository,omitempty"`
 
+	// WorkspaceName is a short, unique description of the changes contained in this package revision.
+	WorkspaceName WorkspaceName `json:"workspaceName,omitempty"`
+
+	// Revision identifies the version of the package.
+	Revision string `json:"revision,omitempty"`
+
 	// Parent references a package that provides resources to us
 	Parent *ParentReference `json:"parent,omitempty"`
 
@@ -92,6 +97,7 @@ type ParentReference struct {
 
 // PackageRevisionStatus defines the observed state of PackageRevision
 type PackageRevisionStatus struct {
+	// UpstreamLock identifies the upstream data for this package.
 	UpstreamLock *UpstreamLock `json:"upstreamLock,omitempty"`
 
 	// PublishedBy is the identity of the user who approved the packagerevision.
diff --git a/porch/api/porch/v1alpha1/zz_generated.conversion.go b/porch/api/porch/v1alpha1/zz_generated.conversion.go
index cf99c346..949f9c38 100644
--- a/porch/api/porch/v1alpha1/zz_generated.conversion.go
+++ b/porch/api/porch/v1alpha1/zz_generated.conversion.go
@@ -1139,6 +1139,7 @@ func Convert_porch_PackageRevisionResourcesList_To_v1alpha1_PackageRevisionResou
 
 func autoConvert_v1alpha1_PackageRevisionResourcesSpec_To_porch_PackageRevisionResourcesSpec(in *PackageRevisionResourcesSpec, out *porch.PackageRevisionResourcesSpec, s conversion.Scope) error {
 	out.PackageName = in.PackageName
+	out.WorkspaceName = porch.WorkspaceName(in.WorkspaceName)
 	out.Revision = in.Revision
 	out.RepositoryName = in.RepositoryName
 	out.Resources = *(*map[string]string)(unsafe.Pointer(&in.Resources))
@@ -1152,6 +1153,7 @@ func Convert_v1alpha1_PackageRevisionResourcesSpec_To_porch_PackageRevisionResou
 
 func autoConvert_porch_PackageRevisionResourcesSpec_To_v1alpha1_PackageRevisionResourcesSpec(in *porch.PackageRevisionResourcesSpec, out *PackageRevisionResourcesSpec, s conversion.Scope) error {
 	out.PackageName = in.PackageName
+	out.WorkspaceName = WorkspaceName(in.WorkspaceName)
 	out.Revision = in.Revision
 	out.RepositoryName = in.RepositoryName
 	out.Resources = *(*map[string]string)(unsafe.Pointer(&in.Resources))
@@ -1189,8 +1191,9 @@ func Convert_porch_PackageRevisionResourcesStatus_To_v1alpha1_PackageRevisionRes
 
 func autoConvert_v1alpha1_PackageRevisionSpec_To_porch_PackageRevisionSpec(in *PackageRevisionSpec, out *porch.PackageRevisionSpec, s conversion.Scope) error {
 	out.PackageName = in.PackageName
-	out.Revision = in.Revision
 	out.RepositoryName = in.RepositoryName
+	out.WorkspaceName = porch.WorkspaceName(in.WorkspaceName)
+	out.Revision = in.Revision
 	out.Parent = (*porch.ParentReference)(unsafe.Pointer(in.Parent))
 	out.Lifecycle = porch.PackageRevisionLifecycle(in.Lifecycle)
 	out.Tasks = *(*[]porch.Task)(unsafe.Pointer(&in.Tasks))
@@ -1205,8 +1208,9 @@ func Convert_v1alpha1_PackageRevisionSpec_To_porch_PackageRevisionSpec(in *Packa
 
 func autoConvert_porch_PackageRevisionSpec_To_v1alpha1_PackageRevisionSpec(in *porch.PackageRevisionSpec, out *PackageRevisionSpec, s conversion.Scope) error {
 	out.PackageName = in.PackageName
-	out.Revision = in.Revision
 	out.RepositoryName = in.RepositoryName
+	out.WorkspaceName = WorkspaceName(in.WorkspaceName)
+	out.Revision = in.Revision
 	out.Parent = (*ParentReference)(unsafe.Pointer(in.Parent))
 	out.Lifecycle = PackageRevisionLifecycle(in.Lifecycle)
 	out.Tasks = *(*[]Task)(unsafe.Pointer(&in.Tasks))
diff --git a/porch/go.mod b/porch/go.mod
index e748b8cd..8867470b 100644
--- a/porch/go.mod
+++ b/porch/go.mod
@@ -17,7 +17,7 @@ require (
 	github.com/GoogleContainerTools/kpt-functions-catalog/functions/go/set-namespace v0.4.1
 	github.com/GoogleContainerTools/kpt-functions-catalog/functions/go/starlark v0.4.3
 	github.com/GoogleContainerTools/kpt-functions-sdk/go/fn v0.0.0-20220506190241-f85503febd54
-	github.com/GoogleContainerTools/kpt/porch/api v0.0.0-20220821193112-4792e5fa18ee
+	github.com/GoogleContainerTools/kpt/porch/api v0.0.0-20221027171429-622c0141f3d7
 	github.com/bluekeyes/go-gitdiff v0.6.1
 	github.com/go-git/go-billy/v5 v5.3.1
 	github.com/go-git/go-git/v5 v5.4.3-0.20220408232334-4f916225cb2f
diff --git a/porch/pkg/cache/repository.go b/porch/pkg/cache/repository.go
index a019a210..7bd68719 100644
--- a/porch/pkg/cache/repository.go
+++ b/porch/pkg/cache/repository.go
@@ -225,6 +225,17 @@ func (r *cachedRepository) update(ctx context.Context, updated repository.Packag
 	k := updated.Key()
 	// previous := r.cachedPackageRevisions[k]
 
+	if updated.Lifecycle() == v1alpha1.PackageRevisionLifecyclePublished {
+		oldKey := repository.PackageRevisionKey{
+			Repository:    k.Repository,
+			Package:       k.Package,
+			WorkspaceName: k.WorkspaceName,
+		}
+		if _, ok := r.cachedPackageRevisions[oldKey]; ok {
+			delete(r.cachedPackageRevisions, oldKey)
+		}
+	}
+
 	cached := &cachedPackageRevision{PackageRevision: updated}
 	r.cachedPackageRevisions[k] = cached
 
diff --git a/porch/pkg/engine/engine.go b/porch/pkg/engine/engine.go
index c858bd34..7887bfc8 100644
--- a/porch/pkg/engine/engine.go
+++ b/porch/pkg/engine/engine.go
@@ -271,6 +271,20 @@ func (cad *cadEngine) CreatePackageRevision(ctx context.Context, repositoryObj *
 		return nil, err
 	}
 
+	if err := repository.ValidateWorkspaceName(obj.Spec.WorkspaceName); err != nil {
+		return nil, fmt.Errorf("failed to create packagerevision: %w", err)
+	}
+
+	// The workspaceName must be unique, because it used to generate the package revision's metadata.name.
+	revs, err := repo.ListPackageRevisions(ctx, repository.ListPackageRevisionFilter{Package: obj.Spec.PackageName, WorkspaceName: obj.Spec.WorkspaceName})
+	if err != nil {
+		return nil, fmt.Errorf("error searching through existing package revisions: %w", err)
+	}
+	if len(revs) != 0 {
+		return nil, fmt.Errorf("package revision workspaceNames must be unique; package revision with name %s in repo %s with "+
+			"workspaceName %s already exists", obj.Spec.PackageName, obj.Spec.RepositoryName, obj.Spec.WorkspaceName)
+	}
+
 	sameOrigin, err := cad.ensureSameOrigin(ctx, obj, repo)
 	if err != nil {
 		return nil, fmt.Errorf("error ensuring same origin: %w", err)
diff --git a/porch/pkg/git/annotation.go b/porch/pkg/git/annotation.go
index ee1f5404..025e983c 100644
--- a/porch/pkg/git/annotation.go
+++ b/porch/pkg/git/annotation.go
@@ -33,6 +33,10 @@ type gitAnnotation struct {
 	// without having to check file paths.
 	PackagePath string `json:"package,omitempty"`
 
+	// WorkspaceName holds the workspaceName of the package revision the commit
+	// belongs to.
+	WorkspaceName v1alpha1.WorkspaceName `json:"workspaceName,omitempty"`
+
 	// Revision hold the revision of the package revision the commit
 	// belongs to.
 	Revision string `json:"revision,omitempty"`
diff --git a/porch/pkg/git/draft.go b/porch/pkg/git/draft.go
index 1e4e177e..3e45c1d2 100644
--- a/porch/pkg/git/draft.go
+++ b/porch/pkg/git/draft.go
@@ -33,16 +33,27 @@ import (
 )
 
 type gitPackageDraft struct {
-	parent    *gitRepository
-	path      string
-	revision  string
-	lifecycle v1alpha1.PackageRevisionLifecycle // New value of the package revision lifecycle
-	updated   time.Time
-	base      *plumbing.Reference // ref to the base of the package update commit chain (used for conditional push)
-	branch    BranchName          // name of the branch where the changes will be pushed
-	commit    plumbing.Hash       // Current HEAD of the package changes (commit sha)
-	tree      plumbing.Hash       // Cached tree of the package itself, some descendent of commit.Tree()
-	tasks     []v1alpha1.Task
+	parent        *gitRepository
+	path          string
+	revision      string
+	workspaceName v1alpha1.WorkspaceName
+	updated       time.Time
+	tasks         []v1alpha1.Task
+
+	// New value of the package revision lifecycle
+	lifecycle v1alpha1.PackageRevisionLifecycle
+
+	// ref to the base of the package update commit chain (used for conditional push)
+	base *plumbing.Reference
+
+	// name of the branch where the changes will be pushed
+	branch BranchName
+
+	// Current HEAD of the package changes (commit sha)
+	commit plumbing.Hash
+
+	// Cached tree of the package itself, some descendent of commit.Tree()
+	tree plumbing.Hash
 }
 
 var _ repository.PackageDraft = &gitPackageDraft{}
@@ -71,9 +82,10 @@ func (d *gitPackageDraft) UpdateResources(ctx context.Context, new *v1alpha1.Pac
 	}
 
 	annotation := &gitAnnotation{
-		PackagePath: d.path,
-		Revision:    d.revision,
-		Task:        change,
+		PackagePath:   d.path,
+		WorkspaceName: d.workspaceName,
+		Revision:      d.revision,
+		Task:          change,
 	}
 	message := "Intermediate commit"
 	if change != nil {
@@ -112,13 +124,22 @@ func (d *gitPackageDraft) Close(ctx context.Context) (repository.PackageRevision
 
 func (r *gitRepository) closeDraft(ctx context.Context, d *gitPackageDraft) (*gitPackageRevision, error) {
 	refSpecs := newPushRefSpecBuilder()
-	draftBranch := createDraftName(d.path, d.revision)
-	proposedBranch := createProposedName(d.path, d.revision)
+	draftBranch := createDraftName(d.path, d.workspaceName)
+	proposedBranch := createProposedName(d.path, d.workspaceName)
 
 	var newRef *plumbing.Reference
 
 	switch d.lifecycle {
 	case v1alpha1.PackageRevisionLifecyclePublished:
+		// Finalize the package revision. Assign it a revision number of latest + 1.
+		revisions, err := r.ListPackageRevisions(ctx, repository.ListPackageRevisionFilter{
+			Package: d.path,
+		})
+		d.revision, err = repository.NextRevisionNumber(revisions)
+		if err != nil {
+			return nil, err
+		}
+
 		// Finalize the package revision. Commit it to main branch.
 		commitHash, newTreeHash, commitBase, err := r.commitPackageToMain(ctx, d)
 		if err != nil {
@@ -180,15 +201,22 @@ func (r *gitRepository) closeDraft(ctx context.Context, d *gitPackageDraft) (*gi
 		}
 	}
 
+	// for backwards compatibility with packages that existed before porch supported
+	// descriptions, we populate the workspaceName as the revision number if it is empty
+	if d.workspaceName == "" {
+		d.workspaceName = v1alpha1.WorkspaceName(d.revision)
+	}
+
 	return &gitPackageRevision{
-		repo:     d.parent,
-		path:     d.path,
-		revision: d.revision,
-		updated:  d.updated,
-		ref:      newRef,
-		tree:     d.tree,
-		commit:   newRef.Hash(),
-		tasks:    d.tasks,
+		repo:          d.parent,
+		path:          d.path,
+		revision:      d.revision,
+		workspaceName: d.workspaceName,
+		updated:       d.updated,
+		ref:           newRef,
+		tree:          d.tree,
+		commit:        newRef.Hash(),
+		tasks:         d.tasks,
 	}, nil
 }
 
@@ -259,8 +287,9 @@ func (r *gitRepository) commitPackageToMain(ctx context.Context, d *gitPackageDr
 	// Add a commit without changes to mark that the package revision is approved. The gitAnnotation is
 	// included so that we can later associate the commit with the correct packagerevision.
 	message, err := AnnotateCommitMessage(fmt.Sprintf("Approve %s/%s", packagePath, d.revision), &gitAnnotation{
-		PackagePath: packagePath,
-		Revision:    d.revision,
+		PackagePath:   packagePath,
+		WorkspaceName: d.workspaceName,
+		Revision:      d.revision,
 	})
 	if err != nil {
 		return zero, zero, nil, fmt.Errorf("failed annotation commit message for package %s: %v", packagePath, err)
diff --git a/porch/pkg/git/git.go b/porch/pkg/git/git.go
index 0f79f263..d7f45d74 100644
--- a/porch/pkg/git/git.go
+++ b/porch/pkg/git/git.go
@@ -286,20 +286,26 @@ func (r *gitRepository) CreatePackageRevision(ctx context.Context, obj *v1alpha1
 		return nil, fmt.Errorf("error when resolving target branch for the package: %w", err)
 	}
 
-	draft := createDraftName(obj.Spec.PackageName, obj.Spec.Revision)
+	if err := repository.ValidateWorkspaceName(obj.Spec.WorkspaceName); err != nil {
+		return nil, fmt.Errorf("failed to create packagerevision: %w", err)
+	}
+
+	// TODO use git branches to leverage uniqueness
+
+	draft := createDraftName(obj.Spec.PackageName, obj.Spec.WorkspaceName)
 
 	// TODO: This should also create a new 'Package' resource if one does not already exist
 
 	return &gitPackageDraft{
-		parent:    r,
-		path:      obj.Spec.PackageName,
-		revision:  obj.Spec.Revision,
-		lifecycle: v1alpha1.PackageRevisionLifecycleDraft,
-		updated:   time.Now(),
-		base:      nil, // Creating a new package
-		tasks:     nil, // Creating a new package
-		branch:    draft,
-		commit:    base,
+		parent:        r,
+		path:          obj.Spec.PackageName,
+		workspaceName: obj.Spec.WorkspaceName,
+		lifecycle:     v1alpha1.PackageRevisionLifecycleDraft,
+		updated:       time.Now(),
+		base:          nil, // Creating a new package
+		tasks:         nil, // Creating a new package
+		branch:        draft,
+		commit:        base,
 	}, nil
 }
 
@@ -331,15 +337,16 @@ func (r *gitRepository) UpdatePackageRevision(ctx context.Context, old repositor
 	}
 
 	return &gitPackageDraft{
-		parent:    r,
-		path:      oldGitPackage.path,
-		revision:  oldGitPackage.revision,
-		lifecycle: oldGitPackage.Lifecycle(),
-		updated:   rev.updated,
-		base:      rev.ref,
-		tree:      rev.tree,
-		commit:    rev.commit,
-		tasks:     rev.tasks,
+		parent:        r,
+		path:          oldGitPackage.path,
+		revision:      oldGitPackage.revision,
+		workspaceName: oldGitPackage.workspaceName,
+		lifecycle:     oldGitPackage.Lifecycle(),
+		updated:       rev.updated,
+		base:          rev.ref,
+		tree:          rev.tree,
+		commit:        rev.commit,
+		tasks:         rev.tasks,
 	}, nil
 }
 
@@ -486,7 +493,27 @@ func (r *gitRepository) loadPackageRevision(ctx context.Context, version, path s
 
 	var ref *plumbing.Reference = nil // Cannot determine ref; this package will be considered final (immutable).
 
-	packageRevision, err := krmPackage.buildGitPackageRevision(ctx, version, ref)
+	var revision string
+	var workspace v1alpha1.WorkspaceName
+	last := strings.LastIndex(version, "/")
+
+	if strings.HasPrefix(version, "drafts/") || strings.HasPrefix(version, "proposed/") {
+		// the passed in version is a ref to an unpublished package revision
+		workspace = v1alpha1.WorkspaceName(version[last+1:])
+	} else {
+		// the passed in version is a ref to a published package revision
+		if version == string(r.branch) || last < 0 {
+			revision = version
+		} else {
+			revision = version[last+1:]
+		}
+		workspace, err = getPkgWorkspace(ctx, commit, krmPackage, ref)
+		if err != nil {
+			return nil, kptfilev1.GitLock{}, err
+		}
+	}
+
+	packageRevision, err := krmPackage.buildGitPackageRevision(ctx, revision, workspace, ref)
 	if err != nil {
 		return nil, lock, err
 	}
@@ -520,7 +547,11 @@ func (r *gitRepository) discoverFinalizedPackages(ctx context.Context, ref *plum
 
 	var result []repository.PackageRevision
 	for _, krmPackage := range krmPackages.packages {
-		packageRevision, err := krmPackage.buildGitPackageRevision(ctx, revision, ref)
+		workspace, err := getPkgWorkspace(ctx, commit, krmPackage, ref)
+		if err != nil {
+			return nil, err
+		}
+		packageRevision, err := krmPackage.buildGitPackageRevision(ctx, revision, workspace, ref)
 		if err != nil {
 			return nil, err
 		}
@@ -531,7 +562,7 @@ func (r *gitRepository) discoverFinalizedPackages(ctx context.Context, ref *plum
 
 // loadDraft will load the draft package.  If the package isn't found (we now require a Kptfile), it will return (nil, nil)
 func (r *gitRepository) loadDraft(ctx context.Context, ref *plumbing.Reference) (*gitPackageRevision, error) {
-	name, revision, err := parseDraftName(ref)
+	name, workspaceName, err := parseDraftName(ref)
 	if err != nil {
 		return nil, err
 	}
@@ -556,7 +587,7 @@ func (r *gitRepository) loadDraft(ctx context.Context, ref *plumbing.Reference)
 		return nil, nil
 	}
 
-	packageRevision, err := krmPackage.buildGitPackageRevision(ctx, revision, ref)
+	packageRevision, err := krmPackage.buildGitPackageRevision(ctx, "", workspaceName, ref)
 	if err != nil {
 		return nil, err
 	}
@@ -564,7 +595,7 @@ func (r *gitRepository) loadDraft(ctx context.Context, ref *plumbing.Reference)
 	return packageRevision, nil
 }
 
-func parseDraftName(draft *plumbing.Reference) (name, revision string, err error) {
+func parseDraftName(draft *plumbing.Reference) (name string, workspaceName v1alpha1.WorkspaceName, err error) {
 	refName := draft.Name()
 	var suffix string
 	if b, ok := getDraftBranchNameInLocal(refName); ok {
@@ -577,10 +608,10 @@ func parseDraftName(draft *plumbing.Reference) (name, revision string, err error
 
 	revIndex := strings.LastIndex(suffix, "/")
 	if revIndex <= 0 {
-		return "", "", fmt.Errorf("invalid draft ref name; missing revision suffix: %q", refName)
+		return "", "", fmt.Errorf("invalid draft ref name; missing workspaceName suffix: %q", refName)
 	}
-	name, revision = suffix[:revIndex], suffix[revIndex+1:]
-	return name, revision, nil
+	name, workspaceName = suffix[:revIndex], v1alpha1.WorkspaceName(suffix[revIndex+1:])
+	return name, workspaceName, nil
 }
 
 func (r *gitRepository) loadTaggedPackages(ctx context.Context, tag *plumbing.Reference) ([]repository.PackageRevision, error) {
@@ -620,7 +651,12 @@ func (r *gitRepository) loadTaggedPackages(ctx context.Context, tag *plumbing.Re
 		return nil, nil
 	}
 
-	packageRevision, err := krmPackage.buildGitPackageRevision(ctx, revision, tag)
+	workspaceName, err := getPkgWorkspace(ctx, commit, krmPackage, tag)
+	if err != nil {
+		return nil, err
+	}
+
+	packageRevision, err := krmPackage.buildGitPackageRevision(ctx, revision, workspaceName, tag)
 	if err != nil {
 		return nil, err
 	}
@@ -893,7 +929,8 @@ func (r *gitRepository) pushAndCleanup(ctx context.Context, ph *pushRefSpecBuild
 	return nil
 }
 
-func (r *gitRepository) loadTasks(ctx context.Context, startCommit *object.Commit, packagePath, revision string) ([]v1alpha1.Task, error) {
+func (r *gitRepository) loadTasks(ctx context.Context, startCommit *object.Commit, packagePath string,
+	workspaceName v1alpha1.WorkspaceName) ([]v1alpha1.Task, error) {
 	var logOptions = git.LogOptions{
 		From:  startCommit.Hash,
 		Order: git.LogOrderCommitterTime,
@@ -927,7 +964,12 @@ func (r *gitRepository) loadTasks(ctx context.Context, startCommit *object.Commi
 		}
 
 		for _, gitAnnotation := range gitAnnotations {
-			if gitAnnotation.PackagePath == packagePath && gitAnnotation.Revision == revision {
+			packageMatches := gitAnnotation.PackagePath == packagePath
+			workspaceNameMatches := gitAnnotation.WorkspaceName == workspaceName ||
+				// this is needed for porch package revisions created before the workspaceName field existed
+				(gitAnnotation.Revision == string(workspaceName) && gitAnnotation.WorkspaceName == "")
+
+			if packageMatches && workspaceNameMatches {
 				// We are iterating through the commits in reverse order.
 				// Tasks that are read from separate commits will be recorded in
 				// reverse order.
@@ -1084,3 +1126,31 @@ func reverseSlice(s []v1alpha1.Task) {
 		last--
 	}
 }
+
+func getPkgWorkspace(ctx context.Context, commit *object.Commit, p *packageListEntry, ref *plumbing.Reference) (v1alpha1.WorkspaceName, error) {
+	if ref == nil || (!isTagInLocalRepo(ref.Name()) && !isDraftBranchNameInLocal(ref.Name()) && !isProposedBranchNameInLocal(ref.Name())) {
+		// packages on the main branch may have unrelated commits, we need to find the latest commit relevant to this package
+		c, err := p.parent.parent.findLatestPackageCommit(ctx, p.parent.commit, p.path)
+		if err != nil {
+			return "", err
+		}
+		if c != nil {
+			commit = c
+		}
+	}
+	annotations, err := ExtractGitAnnotations(commit)
+	if err != nil {
+		return "", err
+	}
+	workspaceName := v1alpha1.WorkspaceName("")
+	for _, a := range annotations {
+		if a.PackagePath != p.path {
+			continue
+		}
+		if a.WorkspaceName != "" {
+			workspaceName = a.WorkspaceName
+			break
+		}
+	}
+	return workspaceName, nil
+}
diff --git a/porch/pkg/git/package.go b/porch/pkg/git/package.go
index 99179934..a23e191f 100644
--- a/porch/pkg/git/package.go
+++ b/porch/pkg/git/package.go
@@ -32,15 +32,16 @@ import (
 )
 
 type gitPackageRevision struct {
-	repo      *gitRepository // repo is repo containing the package
-	path      string
-	revision  string
-	updated   time.Time
-	updatedBy string
-	ref       *plumbing.Reference // ref is the Git reference at which the package exists
-	tree      plumbing.Hash       // Cached tree of the package itself, some descendent of commit.Tree()
-	commit    plumbing.Hash       // Current version of the package (commit sha)
-	tasks     []v1alpha1.Task
+	repo          *gitRepository // repo is repo containing the package
+	path          string
+	revision      string
+	workspaceName v1alpha1.WorkspaceName
+	updated       time.Time
+	updatedBy     string
+	ref           *plumbing.Reference // ref is the Git reference at which the package exists
+	tree          plumbing.Hash       // Cached tree of the package itself, some descendent of commit.Tree()
+	commit        plumbing.Hash       // Current version of the package (commit sha)
+	tasks         []v1alpha1.Task
 }
 
 var _ repository.PackageRevision = &gitPackageRevision{}
@@ -54,7 +55,16 @@ var _ repository.PackageRevision = &gitPackageRevision{}
 // layer, the prefix will be removed (this may happen without notice) so it should not
 // be relied upon by clients.
 func (p *gitPackageRevision) KubeObjectName() string {
-	hash := sha1.Sum([]byte(fmt.Sprintf("%s:%s:%s", p.repo.name, p.path, p.revision)))
+	// The published package revisions on the main branch will have the same workspaceName
+	// as the most recently published package revision, so we need to ensure it has a unique
+	// and unchanging name.
+	var s string
+	if p.revision == string(p.repo.branch) {
+		s = p.revision
+	} else {
+		s = string(p.workspaceName)
+	}
+	hash := sha1.Sum([]byte(fmt.Sprintf("%s:%s:%s", p.repo.name, p.path, s)))
 	return p.repo.name + "-" + hex.EncodeToString(hash[:])
 }
 
@@ -64,22 +74,29 @@ func (p *gitPackageRevision) KubeObjectNamespace() string {
 
 func (p *gitPackageRevision) Key() repository.PackageRevisionKey {
 	return repository.PackageRevisionKey{
-		Repository: p.repo.name,
-		Package:    p.path,
-		Revision:   p.revision,
+		Repository:    p.repo.name,
+		Package:       p.path,
+		Revision:      p.revision,
+		WorkspaceName: p.workspaceName,
 	}
 }
 
 func (p *gitPackageRevision) uid() types.UID {
-	return types.UID(fmt.Sprintf("uid:%s:%s", p.path, p.revision))
+	var s string
+	if p.revision == string(p.repo.branch) {
+		s = p.revision
+	} else {
+		s = string(p.workspaceName)
+	}
+	return types.UID(fmt.Sprintf("uid:%s:%s", p.path, s))
 }
 
 func (p *gitPackageRevision) GetPackageRevision(ctx context.Context) (*v1alpha1.PackageRevision, error) {
 	key := p.Key()
 
 	_, lock, _ := p.GetUpstreamLock(ctx)
-
 	lockCopy := &v1alpha1.UpstreamLock{}
+
 	// TODO: Use kpt definition of UpstreamLock in the package revision status
 	// when https://github.com/GoogleContainerTools/kpt/issues/3297 is complete.
 	// Until then, we have to translate from one type to another.
@@ -128,12 +145,12 @@ func (p *gitPackageRevision) GetPackageRevision(ctx context.Context) (*v1alpha1.
 		},
 		Spec: v1alpha1.PackageRevisionSpec{
 			PackageName:    key.Package,
-			Revision:       key.Revision,
 			RepositoryName: key.Repository,
-
 			Lifecycle:      p.Lifecycle(),
 			Tasks:          p.tasks,
 			ReadinessGates: repository.ToApiReadinessGates(kf),
+			WorkspaceName:  key.WorkspaceName,
+			Revision:       key.Revision,
 		},
 		Status: status,
 	}, nil
@@ -164,6 +181,7 @@ func (p *gitPackageRevision) GetResources(ctx context.Context) (*v1alpha1.Packag
 		},
 		Spec: v1alpha1.PackageRevisionResourcesSpec{
 			PackageName:    key.Package,
+			WorkspaceName:  key.WorkspaceName,
 			Revision:       key.Revision,
 			RepositoryName: key.Repository,
 
diff --git a/porch/pkg/git/package_tree.go b/porch/pkg/git/package_tree.go
index e0611426..97802524 100644
--- a/porch/pkg/git/package_tree.go
+++ b/porch/pkg/git/package_tree.go
@@ -20,6 +20,7 @@ import (
 	"path"
 	"time"
 
+	"github.com/GoogleContainerTools/kpt/porch/api/porch/v1alpha1"
 	"github.com/go-git/go-git/v5/plumbing"
 	"github.com/go-git/go-git/v5/plumbing/filemode"
 	"github.com/go-git/go-git/v5/plumbing/object"
@@ -52,9 +53,9 @@ type packageListEntry struct {
 
 // buildGitPackageRevision creates a gitPackageRevision for the packageListEntry
 // TODO: Can packageListEntry just _be_ a gitPackageRevision?
-func (p *packageListEntry) buildGitPackageRevision(ctx context.Context, revision string, ref *plumbing.Reference) (*gitPackageRevision, error) {
+func (p *packageListEntry) buildGitPackageRevision(ctx context.Context, revision string, workspace v1alpha1.WorkspaceName, ref *plumbing.Reference) (*gitPackageRevision, error) {
 	repo := p.parent.parent
-	tasks, err := repo.loadTasks(ctx, p.parent.commit, p.path, revision)
+	tasks, err := repo.loadTasks(ctx, p.parent.commit, p.path, workspace)
 	if err != nil {
 		return nil, err
 	}
@@ -88,16 +89,23 @@ func (p *packageListEntry) buildGitPackageRevision(ctx context.Context, revision
 		// operation.
 	}
 
+	// for backwards compatibility with packages that existed before porch supported
+	// workspaceNames, we populate the workspaceName as the revision number if it is empty
+	if workspace == "" {
+		workspace = v1alpha1.WorkspaceName(revision)
+	}
+
 	return &gitPackageRevision{
-		repo:      repo,
-		path:      p.path,
-		revision:  revision,
-		updated:   updated,
-		updatedBy: updatedBy,
-		ref:       ref,
-		tree:      p.treeHash,
-		commit:    p.parent.commit.Hash,
-		tasks:     tasks,
+		repo:          repo,
+		path:          p.path,
+		workspaceName: workspace,
+		revision:      revision,
+		updated:       updated,
+		updatedBy:     updatedBy,
+		ref:           ref,
+		tree:          p.treeHash,
+		commit:        p.parent.commit.Hash,
+		tasks:         tasks,
 	}, nil
 }
 
diff --git a/porch/pkg/git/ref.go b/porch/pkg/git/ref.go
index c369167a..f6377a53 100644
--- a/porch/pkg/git/ref.go
+++ b/porch/pkg/git/ref.go
@@ -18,6 +18,7 @@ import (
 	"fmt"
 	"strings"
 
+	"github.com/GoogleContainerTools/kpt/porch/api/porch/v1alpha1"
 	"github.com/go-git/go-git/v5/config"
 	"github.com/go-git/go-git/v5/plumbing"
 )
@@ -109,12 +110,12 @@ func getTagNameInLocalRepo(n plumbing.ReferenceName) (string, bool) {
 	return trimOptionalPrefix(n.String(), tagsPrefixInLocalRepo)
 }
 
-func createDraftName(pkg, rev string) BranchName {
-	return BranchName(draftsPrefix + pkg + "/" + rev)
+func createDraftName(pkg string, wn v1alpha1.WorkspaceName) BranchName {
+	return BranchName(draftsPrefix + pkg + "/" + string(wn))
 }
 
-func createProposedName(pkg, rev string) BranchName {
-	return BranchName(proposedPrefix + pkg + "/" + rev)
+func createProposedName(pkg string, wn v1alpha1.WorkspaceName) BranchName {
+	return BranchName(proposedPrefix + pkg + "/" + string(wn))
 }
 
 func trimOptionalPrefix(s, prefix string) (string, bool) {
diff --git a/porch/pkg/oci/loader.go b/porch/pkg/oci/loader.go
index a742e154..6c3bcde9 100644
--- a/porch/pkg/oci/loader.go
+++ b/porch/pkg/oci/loader.go
@@ -37,12 +37,13 @@ import (
 
 const (
 	annotationKeyLifecycle = "kpt.dev/lifecycle"
+	annotationKeyRevision  = "kpt.dev/revision"
 )
 
 var tracer = otel.Tracer("oci")
 
 func (r *ociRepository) getLifecycle(ctx context.Context, imageRef oci.ImageDigestName) (v1alpha1.PackageRevisionLifecycle, error) {
-	ctx, span := tracer.Start(ctx, "ociRepository::loadTasks", trace.WithAttributes(
+	ctx, span := tracer.Start(ctx, "ociRepository::getLifecycle", trace.WithAttributes(
 		attribute.Stringer("image", imageRef),
 	))
 	defer span.End()
@@ -70,6 +71,25 @@ func (r *ociRepository) getLifecycle(ctx context.Context, imageRef oci.ImageDige
 	}
 }
 
+func (r *ociRepository) getRevisionNumber(ctx context.Context, imageRef oci.ImageDigestName) (string, error) {
+	ctx, span := tracer.Start(ctx, "ociRepository::getRevision", trace.WithAttributes(
+		attribute.Stringer("image", imageRef),
+	))
+	defer span.End()
+
+	ociImage, err := r.storage.ToRemoteImage(ctx, imageRef)
+	if err != nil {
+		return "", err
+	}
+
+	manifest, err := r.storage.CachedManifest(ctx, ociImage)
+	if err != nil {
+		return "", fmt.Errorf("error fetching manifest for image: %w", err)
+	}
+
+	return manifest.Annotations[annotationKeyRevision], nil
+}
+
 func (r *ociRepository) loadTasks(ctx context.Context, imageRef oci.ImageDigestName) ([]api.Task, error) {
 	ctx, span := tracer.Start(ctx, "ociRepository::loadTasks", trace.WithAttributes(
 		attribute.Stringer("image", imageRef),
diff --git a/porch/pkg/oci/mutate.go b/porch/pkg/oci/mutate.go
index f574d6e6..6588efbb 100644
--- a/porch/pkg/oci/mutate.go
+++ b/porch/pkg/oci/mutate.go
@@ -51,13 +51,17 @@ func (r *ociRepository) CreatePackageRevision(ctx context.Context, obj *api.Pack
 		return nil, err
 	}
 
+	if err := repository.ValidateWorkspaceName(obj.Spec.WorkspaceName); err != nil {
+		return nil, fmt.Errorf("failed to create packagerevision: %w", err)
+	}
+
 	// digestName := ImageDigestName{}
 	return &ociPackageDraft{
 		packageName: packageName,
 		parent:      r,
 		tasks:       []api.Task{},
 		base:        base,
-		tag:         ociRepo.Tag(obj.Spec.Revision),
+		tag:         ociRepo.Tag(string(obj.Spec.WorkspaceName)),
 		lifecycle:   v1alpha1.PackageRevisionLifecycleDraft,
 	}, nil
 }
@@ -65,7 +69,7 @@ func (r *ociRepository) CreatePackageRevision(ctx context.Context, obj *api.Pack
 func (r *ociRepository) UpdatePackageRevision(ctx context.Context, old repository.PackageRevision) (repository.PackageDraft, error) {
 	oldPackage := old.(*ociPackageRevision)
 	packageName := oldPackage.packageName
-	revision := oldPackage.revision
+	workspace := oldPackage.workspaceName
 	// digestName := oldPackage.digestName
 
 	ociRepo, err := name.NewRepository(path.Join(r.spec.Registry, packageName))
@@ -82,7 +86,7 @@ func (r *ociRepository) UpdatePackageRevision(ctx context.Context, old repositor
 		remote.WithContext(ctx),
 	}
 
-	ref := ociRepo.Tag(revision)
+	ref := ociRepo.Tag(string(workspace))
 
 	base, err := remote.Image(ref, options...)
 	if err != nil {
@@ -94,7 +98,7 @@ func (r *ociRepository) UpdatePackageRevision(ctx context.Context, old repositor
 		parent:      r,
 		tasks:       []api.Task{},
 		base:        base,
-		tag:         ociRepo.Tag(revision),
+		tag:         ref,
 		lifecycle:   oldPackage.Lifecycle(),
 	}, nil
 }
@@ -202,6 +206,7 @@ func (p *ociPackageDraft) Close(ctx context.Context) (repository.PackageRevision
 
 	klog.Infof("pushing %s", ref)
 
+	revision := ""
 	addendums := append([]mutate.Addendum{}, p.addendums...)
 	if p.lifecycle != "" {
 		if len(addendums) == 0 {
@@ -219,6 +224,20 @@ func (p *ociPackageDraft) Close(ctx context.Context) (repository.PackageRevision
 				addendum.Annotations = make(map[string]string)
 			}
 			addendum.Annotations[annotationKeyLifecycle] = string(p.lifecycle)
+
+			if p.lifecycle == v1alpha1.PackageRevisionLifecyclePublished {
+				r := p.parent
+				// Finalize the package revision. Assign it a revision number of latest + 1.
+				revisions, err := r.ListPackageRevisions(ctx, repository.ListPackageRevisionFilter{
+					Package: p.packageName,
+				})
+				nextRevisionNumber, err := repository.NextRevisionNumber(revisions)
+				if err != nil {
+					return nil, err
+				}
+				addendum.Annotations[annotationKeyRevision] = nextRevisionNumber
+				revision = nextRevisionNumber
+			}
 		}
 	}
 
@@ -253,7 +272,7 @@ func (p *ociPackageDraft) Close(ctx context.Context) (repository.PackageRevision
 		return nil, fmt.Errorf("error getting config file: %w", err)
 	}
 
-	return p.parent.buildPackageRevision(ctx, digestName, p.packageName, p.tag.TagStr(), configFile.Created.Time)
+	return p.parent.buildPackageRevision(ctx, digestName, p.packageName, v1alpha1.WorkspaceName(p.tag.TagStr()), revision, configFile.Created.Time)
 }
 
 func constructResourceVersion(t time.Time) string {
@@ -267,7 +286,7 @@ func constructUID(ref string) types.UID {
 func (r *ociRepository) DeletePackageRevision(ctx context.Context, old repository.PackageRevision) error {
 	oldPackage := old.(*ociPackageRevision)
 	packageName := oldPackage.packageName
-	revision := oldPackage.revision
+	workspace := oldPackage.workspaceName
 
 	ociRepo, err := name.NewRepository(path.Join(r.spec.Registry, packageName))
 	if err != nil {
@@ -283,7 +302,7 @@ func (r *ociRepository) DeletePackageRevision(ctx context.Context, old repositor
 		remote.WithContext(ctx),
 	}
 
-	ref := ociRepo.Tag(revision)
+	ref := ociRepo.Tag(string(workspace))
 
 	klog.Infof("deleting %s", ref)
 
diff --git a/porch/pkg/oci/oci.go b/porch/pkg/oci/oci.go
index 9a3d1e05..7d43a7ba 100644
--- a/porch/pkg/oci/oci.go
+++ b/porch/pkg/oci/oci.go
@@ -127,12 +127,12 @@ func (r *ociRepository) ListPackageRevisions(ctx context.Context, filter reposit
 						Digest: digest,
 					},
 					packageName:     childName,
-					revision:        tag,
+					workspaceName:   v1alpha1.WorkspaceName(tag),
 					created:         created,
 					parent:          r,
 					resourceVersion: constructResourceVersion(m.Created),
 				}
-				p.uid = constructUID(p.packageName + ":" + p.revision)
+				p.uid = constructUID(p.packageName + ":" + string(p.workspaceName))
 
 				lifecycle, err := r.getLifecycle(ctx, p.digestName)
 				if err != nil {
@@ -140,6 +140,12 @@ func (r *ociRepository) ListPackageRevisions(ctx context.Context, filter reposit
 				}
 				p.lifecycle = lifecycle
 
+				revision, err := r.getRevisionNumber(ctx, p.digestName)
+				if err != nil {
+					return nil, err
+				}
+				p.revision = revision
+
 				tasks, err := r.loadTasks(ctx, p.digestName)
 				if err != nil {
 					return nil, err
@@ -160,7 +166,8 @@ func (r *ociRepository) ListPackages(ctx context.Context, filter repository.List
 	return nil, fmt.Errorf("ListPackages not supported for OCI packages")
 }
 
-func (r *ociRepository) buildPackageRevision(ctx context.Context, name oci.ImageDigestName, packageName string, revision string, created time.Time) (repository.PackageRevision, error) {
+func (r *ociRepository) buildPackageRevision(ctx context.Context, name oci.ImageDigestName, packageName string,
+	workspace v1alpha1.WorkspaceName, revision string, created time.Time) (repository.PackageRevision, error) {
 	if r.content != configapi.RepositoryContentPackage {
 		return nil, fmt.Errorf("repository is not a package repo, type is %v", r.content)
 	}
@@ -168,15 +175,22 @@ func (r *ociRepository) buildPackageRevision(ctx context.Context, name oci.Image
 	ctx, span := tracer.Start(ctx, "ociRepository::buildPackageRevision")
 	defer span.End()
 
+	// for backwards compatibility with packages that existed before porch supported
+	// workspaces, we populate the workspaceName as the revision number if it is empty
+	if workspace == "" {
+		workspace = v1alpha1.WorkspaceName(revision)
+	}
+
 	p := &ociPackageRevision{
 		digestName:      name,
 		packageName:     packageName,
+		workspaceName:   workspace,
 		revision:        revision,
 		created:         created,
 		parent:          r,
 		resourceVersion: constructResourceVersion(created),
 	}
-	p.uid = constructUID(p.packageName + ":" + p.revision)
+	p.uid = constructUID(p.packageName + ":" + string(p.workspaceName))
 
 	lifecycle, err := r.getLifecycle(ctx, p.digestName)
 	if err != nil {
@@ -323,13 +337,13 @@ type ociPackageRevision struct {
 	digestName      oci.ImageDigestName
 	packageName     string
 	revision        string
+	workspaceName   v1alpha1.WorkspaceName
 	created         time.Time
 	resourceVersion string
 	uid             types.UID
 
 	parent *ociRepository
-
-	tasks []v1alpha1.Task
+	tasks  []v1alpha1.Task
 
 	lifecycle v1alpha1.PackageRevisionLifecycle
 }
@@ -360,6 +374,7 @@ func (p *ociPackageRevision) GetResources(ctx context.Context) (*v1alpha1.Packag
 		},
 		Spec: v1alpha1.PackageRevisionResourcesSpec{
 			PackageName:    key.Package,
+			WorkspaceName:  key.WorkspaceName,
 			Revision:       key.Revision,
 			RepositoryName: key.Repository,
 
@@ -369,7 +384,7 @@ func (p *ociPackageRevision) GetResources(ctx context.Context) (*v1alpha1.Packag
 }
 
 func (p *ociPackageRevision) KubeObjectName() string {
-	hash := sha1.Sum([]byte(fmt.Sprintf("%s:%s:%s", p.parent.name, p.packageName, p.revision)))
+	hash := sha1.Sum([]byte(fmt.Sprintf("%s:%s:%s", p.parent.name, p.packageName, p.workspaceName)))
 	return p.parent.name + "-" + hex.EncodeToString(hash[:])
 }
 
@@ -379,9 +394,10 @@ func (p *ociPackageRevision) KubeObjectNamespace() string {
 
 func (p *ociPackageRevision) Key() repository.PackageRevisionKey {
 	return repository.PackageRevisionKey{
-		Repository: p.parent.name,
-		Package:    p.packageName,
-		Revision:   p.revision,
+		Repository:    p.parent.name,
+		Package:       p.packageName,
+		Revision:      p.revision,
+		WorkspaceName: p.workspaceName,
 	}
 }
 
@@ -409,8 +425,9 @@ func (p *ociPackageRevision) GetPackageRevision(ctx context.Context) (*v1alpha1.
 		},
 		Spec: v1alpha1.PackageRevisionSpec{
 			PackageName:    key.Package,
-			Revision:       key.Revision,
 			RepositoryName: key.Repository,
+			Revision:       key.Revision,
+			WorkspaceName:  key.WorkspaceName,
 
 			Lifecycle:      p.Lifecycle(),
 			Tasks:          p.tasks,
diff --git a/porch/pkg/registry/porch/table.go b/porch/pkg/registry/porch/table.go
index a593c13a..ffd14a98 100644
--- a/porch/pkg/registry/porch/table.go
+++ b/porch/pkg/registry/porch/table.go
@@ -116,6 +116,7 @@ var (
 			return []interface{}{
 				pr.Name,
 				pr.Spec.PackageName,
+				pr.Spec.WorkspaceName,
 				pr.Spec.Revision,
 				isLatest(pr),
 				pr.Spec.Lifecycle,
@@ -125,6 +126,7 @@ var (
 		columns: []metav1.TableColumnDefinition{
 			{Name: "Name", Type: "string"},
 			{Name: "Package", Type: "string"},
+			{Name: "WorkspaceName", Type: "string"},
 			{Name: "Revision", Type: "string"},
 			{Name: "Latest", Type: "boolean"},
 			{Name: "Lifecycle", Type: "string"},
@@ -142,6 +144,7 @@ var (
 			return []interface{}{
 				pr.Name,
 				pr.Spec.PackageName,
+				pr.Spec.WorkspaceName,
 				pr.Spec.Revision,
 				pr.Spec.RepositoryName,
 				len(pr.Spec.Resources),
@@ -150,6 +153,7 @@ var (
 		columns: []metav1.TableColumnDefinition{
 			{Name: "Name", Type: "string"},
 			{Name: "Package", Type: "string"},
+			{Name: "WorkspaceName", Type: "string"},
 			{Name: "Revision", Type: "string"},
 			{Name: "Repository", Type: "string"},
 			{Name: "Files", Type: "integer"},
diff --git a/porch/pkg/repository/repository.go b/porch/pkg/repository/repository.go
index 1827e771..6ed8c658 100644
--- a/porch/pkg/repository/repository.go
+++ b/porch/pkg/repository/repository.go
@@ -30,10 +30,12 @@ type PackageResources struct {
 
 type PackageRevisionKey struct {
 	Repository, Package, Revision string
+	WorkspaceName                 v1alpha1.WorkspaceName
 }
 
 func (n PackageRevisionKey) String() string {
-	return fmt.Sprintf("Repository: %q, Package: %q, Revision: %q", n.Repository, n.Package, n.Revision)
+	return fmt.Sprintf("Repository: %q, Package: %q, Revision: %q, WorkspaceName: %q",
+		n.Repository, n.Package, n.Revision, string(n.WorkspaceName))
 }
 
 type PackageKey struct {
@@ -119,6 +121,9 @@ type ListPackageRevisionFilter struct {
 	// Package matches the name of the package (spec.package)
 	Package string
 
+	// WorkspaceName matches the description of the package (spec.workspaceName)
+	WorkspaceName v1alpha1.WorkspaceName
+
 	// Revision matches the revision of the package (spec.revision)
 	Revision string
 }
@@ -131,6 +136,9 @@ func (f *ListPackageRevisionFilter) Matches(p PackageRevision) bool {
 	if f.Revision != "" && f.Revision != p.Key().Revision {
 		return false
 	}
+	if f.WorkspaceName != "" && f.WorkspaceName != p.Key().WorkspaceName {
+		return false
+	}
 	if f.KubeObjectName != "" && f.KubeObjectName != p.KubeObjectName() {
 		return false
 	}
diff --git a/porch/pkg/repository/util.go b/porch/pkg/repository/util.go
index 6c31f8e2..dbd70660 100644
--- a/porch/pkg/repository/util.go
+++ b/porch/pkg/repository/util.go
@@ -16,9 +16,14 @@ package repository
 
 import (
 	"fmt"
+	"regexp"
+	"strconv"
+	"strings"
 
 	kptfile "github.com/GoogleContainerTools/kpt/pkg/api/kptfile/v1"
+	"github.com/GoogleContainerTools/kpt/porch/api/porch/v1alpha1"
 	api "github.com/GoogleContainerTools/kpt/porch/api/porch/v1alpha1"
+	"golang.org/x/mod/semver"
 )
 
 func ToApiReadinessGates(kf kptfile.KptFile) []api.ReadinessGate {
@@ -60,3 +65,78 @@ func toApiConditionStatus(s kptfile.ConditionStatus) api.ConditionStatus {
 		panic(fmt.Errorf("unknown condition status: %v", s))
 	}
 }
+
+const revisionRegex = "^v[0-9]+$"
+
+func NextRevisionNumber(revs []PackageRevision) (string, error) {
+	// Computes the next revision number as the latest revision number + 1.
+	// This function only understands strict versioning format, e.g. v1, v2, etc. It will
+	// ignore all revision numbers it finds that do not adhere to this format.
+	// If there are no published revisions (in the recognized format), the revision
+	// number returned here will be "v1".
+
+	latestRev := "v0"
+	for _, current := range revs {
+
+		// Check if the current package revision is more recent than the one seen so far.
+		// Only consider Published packages
+		if current.Lifecycle() != v1alpha1.PackageRevisionLifecyclePublished {
+			continue
+		}
+
+		currentRev := current.Key().Revision
+		if !semver.IsValid(currentRev) {
+			// ignore this revision
+			continue
+		}
+		// collect the major version. i.e. if we find that the latest published
+		// version is v3.1.1, we will end up returning v4
+		currentRev = semver.Major(currentRev)
+
+		switch cmp := semver.Compare(currentRev, latestRev); {
+		case cmp == 0:
+			// Same revision.
+		case cmp < 0:
+			// current < latest; no change
+		case cmp > 0:
+			// current > latest; update latest
+			latestRev = currentRev
+		}
+
+	}
+
+	i, err := strconv.Atoi(latestRev[1:])
+	if err != nil {
+		return "", err
+	}
+	i++
+	next := "v" + strconv.Itoa(i)
+	return next, nil
+}
+
+// ValidateWorkspaceName validates WorkspaceName. It must:
+//   - be at least 1 and at most 63 characters long
+//   - contain only lowercase alphanumeric characters or '-'
+//   - start and end with an alphanumeric character.
+//
+// '/ ' should never be allowed, because we use '/' to
+// delimit branch names (e.g. the 'drafts/' prefix).
+func ValidateWorkspaceName(workspace v1alpha1.WorkspaceName) error {
+	wn := string(workspace)
+	if len(wn) > 63 || len(wn) == 0 {
+		return fmt.Errorf("workspaceName %q must be at least 1 and at most 63 characters long", wn)
+	}
+	if strings.HasPrefix(wn, "-") || strings.HasSuffix(wn, "-") {
+		return fmt.Errorf("workspaceName %q must start and end with an alphanumeric character", wn)
+	}
+
+	match, err := regexp.MatchString(`^[a-z0-9-]+$`, wn)
+	if err != nil {
+		return err
+	}
+	if !match {
+		return fmt.Errorf("workspaceName %q must be comprised only of lowercase alphanumeric characters and '-'", wn)
+	}
+
+	return nil
+}
diff --git a/site/guides/porch-ui-installation.md b/site/guides/porch-ui-installation.md
index 0b78d878..f2e82b0c 100644
--- a/site/guides/porch-ui-installation.md
+++ b/site/guides/porch-ui-installation.md
@@ -1,24 +1,20 @@
 # Accessing the Configuration as Data UI
 
-The easiest way to access the Configuration as Data UI is running by a docker
-container on your local machine where you'll be able to access the UI with your
-browser. Running the container locally simplifies the overall setup by allowing
-the UI to use your local kubeconfig and Google credentials to access the kubernetes
-cluster with Porch installed. This guide will show you how to do this.
+You can access the Configuration as Data UI UI either by running the UI on a
+cluster or integrating the UI into an existing Backstage installation.
 
 ## Prerequisites
 
-To access the Configuration as Data UI with a docker container, you will need:
+To access the Configuration as Data UI, you will need:
 
-*   [Porch](guides/porch-installation.md) installed on a kubernetes cluster
-*   [kubectl](https://kubernetes.io/docs/tasks/tools/) targeting the kubernetes cluster
-    with Porch installed
-*   [git](https://git-scm.com/)
-*   [docker](https://docs.docker.com/get-docker/)
+- [Porch](guides/porch-installation.md) installed on a Kubernetes cluster
+- [kubectl](https://kubernetes.io/docs/tasks/tools/) targeting the Kubernetes
+  cluster with Porch installed
+- [kpt CLI](https://kpt.dev/installation/kpt-cli) installed
 
 ## Running on a GKE cluster
 
-This setup assumes that you have a GKE cluster up and running with porch installed, and that
+This setup assumes that you have a GKE cluster up and running with Porch installed, and that
 your current kube context is set to that GKE cluster. We would welcome contributions or feedback
 from people that have set this up in other clouds outside of GKE.
 
@@ -137,38 +133,6 @@ kubectl port-forward --namespace=backstage svc/backstage 7007
 Open the plugin by browsing to `localhost:7007/config-as-data`. On the plugin, you will need to sign in to your
 Google account so that the plugin can access your GKE cluster.
 
-## Running locally in a container
-
-This setup is intended for those developing the plugin. These instructions assume GKE and workload identity,
-to simplify authentication configuration, but we would welcome contributions or feedback from people that have set
-this up in other clouds.
-
-First, clone the
-[kpt-backstage-plugins](https://github.com/GoogleContainerTools/kpt-backstage-plugins)
-repository.
-
-```sh
-git clone https://github.com/GoogleContainerTools/kpt-backstage-plugins.git
-cd kpt-backstage-plugins
-```
-
-Next, build the kpt-backstage-plugins image.
-
-```sh
-docker build --target backstage-app-local --tag kpt-backstage-plugins .
-```
-
-And create a new container using the kpt-backstage-plugins image. The two
-attached volumnes allows the UI to connect to your GKE using your local Google
-credentials, and the UI will be exposed over port 7007.
-
-```sh
-docker run -v ~/.kube/config:/root/.kube/config -v ~/.config/gcloud:/root/.config/gcloud -p 7007:7007 kpt-backstage-plugins
-```
-
-And now access the Configuration as Data UI by opening your browser to
-http://localhost:7007/config-as-data.
-
 ## Running in Backstage
 
 This setup is intended for those installing the plugin into existing backstage deployments.


Test patch:
diff --git a/.github/workflows/porch-e2e.yml b/.github/workflows/porch-e2e.yml
index 0f8251f1..7f00418d 100644
--- a/.github/workflows/porch-e2e.yml
+++ b/.github/workflows/porch-e2e.yml
@@ -97,5 +97,7 @@ jobs:
       - name: e2e test
         run: E2E=1 go test -v .
         working-directory: ./porch/test/e2e
-      - name: Porch CLI e2e test
-        run: make test-porch
+      # TODO (natasha41575): Reenable the porch CLI tests in a followup PR that fixes the CLI
+      # to use the new package revision spec.description field. 
+      # - name: Porch CLI e2e test
+      #   run: make test-porch
diff --git a/commands/alpha/rpkg/copy/command_test.go b/commands/alpha/rpkg/copy/command_test.go
deleted file mode 100644
index a89542f3..00000000
--- a/commands/alpha/rpkg/copy/command_test.go
+++ /dev/null
@@ -1,59 +0,0 @@
-// Copyright 2022 Google LLC
-//
-// 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 copy
-
-import (
-	"testing"
-
-	"github.com/stretchr/testify/require"
-)
-
-func TestNextRevisionNumber(t *testing.T) {
-	testcases := map[string]struct {
-		input    string
-		expected string
-		err      string
-	}{
-		"invalid": {
-			input: "alskdfj",
-			err:   "invalid revision format alskdfj; explicit --revision flag is required",
-		},
-		"no dots": {
-			input:    "v4",
-			expected: "v5",
-		},
-		"one dot": {
-			input:    "v3.1",
-			expected: "v3.2",
-		},
-		"two dots": {
-			input:    "v1.2.3",
-			expected: "v1.2.4",
-		},
-	}
-
-	for name, tc := range testcases {
-		t.Run(name, func(t *testing.T) {
-			output, err := nextRevisionNumber(tc.input)
-			if tc.err != "" {
-				require.Error(t, err)
-				require.Equal(t, tc.err, err.Error())
-			} else {
-				require.NoError(t, err)
-				require.Equal(t, tc.expected, output)
-			}
-		})
-	}
-}
diff --git a/e2e/testdata/porch/rpkg-clone/config.yaml b/e2e/testdata/porch/rpkg-clone/config.yaml
index 6cecf5a4..05ba6bf2 100644
--- a/e2e/testdata/porch/rpkg-clone/config.yaml
+++ b/e2e/testdata/porch/rpkg-clone/config.yaml
@@ -13,9 +13,9 @@ commands:
       - --namespace=rpkg-clone
       - https://github.com/platkrm/test-blueprints.git/basens@basens/v1
       - --repository=git
-      - --revision=v0
+      - --workspace=clone-2
       - basens-clone
-    stdout: "git-317a42f437be4e9695da4f043ff2c727f6001156 created\n"
+    stdout: "git-3465eed5831e5c372243d048631c8ef1666b47d6 created\n"
   - args:
       - alpha
       - rpkg
@@ -23,7 +23,7 @@ commands:
       - --namespace=rpkg-clone
       - https://github.com/platkrm/test-blueprints.git/basens@basens/v1
       - --repository=git
-      - --revision=v1
+      - --workspace=clone-3
       - basens-clone
     stderr: "error: `clone` cannot create a new revision for package \"basens-clone\" that already exists in repo \"git\"; make subsequent revisions using `copy`\n"
     exitCode: 1
@@ -49,15 +49,15 @@ commands:
       - --namespace=rpkg-clone
       - test-blueprints-e78ee77d9560703561c2656c97c77e9abb8c4c53
       - --repository=git
-      - --revision=v0
+      - --workspace=clone-1
       - empty-clone
-    stdout: "git-94533e00ee86a2edd5e8d91cbb76cf8ef295b58c created\n"
+    stdout: "git-b67f9ce14d378317ba83c9504eab9cc024932dd3 created\n"
   - args:
       - alpha
       - rpkg
       - pull
       - --namespace=rpkg-clone
-      - git-317a42f437be4e9695da4f043ff2c727f6001156
+      - git-3465eed5831e5c372243d048631c8ef1666b47d6
     stdout: |
       apiVersion: config.kubernetes.io/v1
       items:
@@ -103,7 +103,7 @@ commands:
       - rpkg
       - pull
       - --namespace=rpkg-clone
-      - git-94533e00ee86a2edd5e8d91cbb76cf8ef295b58c
+      - git-b67f9ce14d378317ba83c9504eab9cc024932dd3
     stdout: |
       apiVersion: config.kubernetes.io/v1
       items:
diff --git a/e2e/testdata/porch/rpkg-copy/config.yaml b/e2e/testdata/porch/rpkg-copy/config.yaml
index 9dc3ec18..c0960ba7 100644
--- a/e2e/testdata/porch/rpkg-copy/config.yaml
+++ b/e2e/testdata/porch/rpkg-copy/config.yaml
@@ -15,36 +15,37 @@ commands:
       - --directory=basens
       - --ref=basens/v1
       - --repository=git
-      - --revision=v0
+      - --workspace=copy-1
       - basens-edit
-    stdout: "git-f1fbc2b72ee70b2f34e64c2630031d0560e6cf48 created\n"
+    stdout: "git-eb5afe755bedd142f142c6a9363649c667ef77a5 created\n"
   - args:
       - alpha
       - rpkg
       - propose
       - --namespace=rpkg-copy
-      - git-f1fbc2b72ee70b2f34e64c2630031d0560e6cf48
-    stderr: "git-f1fbc2b72ee70b2f34e64c2630031d0560e6cf48 proposed\n"
+      - git-eb5afe755bedd142f142c6a9363649c667ef77a5
+    stderr: "git-eb5afe755bedd142f142c6a9363649c667ef77a5 proposed\n"
   - args:
       - alpha
       - rpkg
       - approve
       - --namespace=rpkg-copy
-      - git-f1fbc2b72ee70b2f34e64c2630031d0560e6cf48
-    stderr: "git-f1fbc2b72ee70b2f34e64c2630031d0560e6cf48 approved\n"
+      - git-eb5afe755bedd142f142c6a9363649c667ef77a5
+    stderr: "git-eb5afe755bedd142f142c6a9363649c667ef77a5 approved\n"
   - args:
       - alpha
       - rpkg
       - copy
       - --namespace=rpkg-copy
-      - git-f1fbc2b72ee70b2f34e64c2630031d0560e6cf48
-    stdout: "git-66579ccd78cf67f80a3cafaf63dc121db4d92b83 created\n"
+      - --workspace=copy-2
+      - git-eb5afe755bedd142f142c6a9363649c667ef77a5
+    stdout: "git-a29df72d1135fd010ea49f4d4877001dee423be6 created\n"
   - args:
       - alpha
       - rpkg
       - pull
       - --namespace=rpkg-copy
-      - git-66579ccd78cf67f80a3cafaf63dc121db4d92b83
+      - git-a29df72d1135fd010ea49f4d4877001dee423be6
     stdout: |
       apiVersion: config.kubernetes.io/v1
       items:
diff --git a/e2e/testdata/porch/rpkg-get/config.yaml b/e2e/testdata/porch/rpkg-get/config.yaml
index 034b4f35..9a3af86c 100644
--- a/e2e/testdata/porch/rpkg-get/config.yaml
+++ b/e2e/testdata/porch/rpkg-get/config.yaml
@@ -28,8 +28,8 @@ commands:
       - --namespace=rpkg-get
       - test-blueprints-526fa27229adcc3b6a9a544c455c344a3b4d7597
     stdout: |
-      NAME                                                       PACKAGE   REVISION   LATEST   LIFECYCLE   REPOSITORY
-      test-blueprints-526fa27229adcc3b6a9a544c455c344a3b4d7597   basens    v1         false    Published   test-blueprints
+      NAME                                                       PACKAGE   WORKSPACENAME   REVISION   LATEST   LIFECYCLE   REPOSITORY
+      test-blueprints-526fa27229adcc3b6a9a544c455c344a3b4d7597   basens    v1              v1         false    Published   test-blueprints
   - args:
       - alpha
       - rpkg
@@ -37,8 +37,8 @@ commands:
       - --namespace=rpkg-get
       - --name=basens
     stdout: |
-      NAME                                                       PACKAGE   REVISION   LATEST   LIFECYCLE   REPOSITORY
-      test-blueprints-9626794e984ff13c9a4c64df5af0f15ec3a146bf   basens    main       false    Published   test-blueprints
-      test-blueprints-526fa27229adcc3b6a9a544c455c344a3b4d7597   basens    v1         false    Published   test-blueprints
-      test-blueprints-499689d1e0c6fced058158330d922c004367d6cd   basens    v2         false    Published   test-blueprints
-      test-blueprints-913ab85d2d49621636a0ffa514a2a008e6a7012e   basens    v3         true     Published   test-blueprints
+      NAME                                                       PACKAGE   WORKSPACENAME   REVISION   LATEST   LIFECYCLE   REPOSITORY
+      test-blueprints-9626794e984ff13c9a4c64df5af0f15ec3a146bf   basens    main            main       false    Published   test-blueprints
+      test-blueprints-526fa27229adcc3b6a9a544c455c344a3b4d7597   basens    v1              v1         false    Published   test-blueprints
+      test-blueprints-499689d1e0c6fced058158330d922c004367d6cd   basens    v2              v2         false    Published   test-blueprints
+      test-blueprints-913ab85d2d49621636a0ffa514a2a008e6a7012e   basens    v3              v3         true     Published   test-blueprints
diff --git a/e2e/testdata/porch/rpkg-init-deploy/config.yaml b/e2e/testdata/porch/rpkg-init-deploy/config.yaml
index 12b4952a..b030da32 100644
--- a/e2e/testdata/porch/rpkg-init-deploy/config.yaml
+++ b/e2e/testdata/porch/rpkg-init-deploy/config.yaml
@@ -27,15 +27,15 @@ commands:
       - --site
       - http://kpt.dev/deploy-package
       - --repository=git
-      - --revision=v0
+      - --workspace=deploy
       - deploy-package
-    stdout: "git-480992fb1bc6d0e30d69d3ae84101d7413e1a5ae created\n"
+    stdout: "git-628abd0a0903f5de6cb3604d917724f6fc1b5e08 created\n"
   - args:
       - alpha
       - rpkg
       - pull
       - --namespace=rpkg-init-deploy
-      - git-480992fb1bc6d0e30d69d3ae84101d7413e1a5ae
+      - git-628abd0a0903f5de6cb3604d917724f6fc1b5e08
     stdout: |
       apiVersion: config.kubernetes.io/v1
       items:
diff --git a/e2e/testdata/porch/rpkg-init/config.yaml b/e2e/testdata/porch/rpkg-init/config.yaml
index a4be93c2..db2dd906 100644
--- a/e2e/testdata/porch/rpkg-init/config.yaml
+++ b/e2e/testdata/porch/rpkg-init/config.yaml
@@ -26,15 +26,15 @@ commands:
       - --site
       - http://kpt.dev/init-package
       - --repository=git
-      - --revision=v0
+      - --workspace=init-1
       - init-package
-    stdout: "git-e5041a7290c88ac4ac34182c8b3d658ab97d353d created\n"
+    stdout: "git-95686470a1fd3a3ba726cce4c8f449f6bbe2b02a created\n"
   - args:
       - alpha
       - rpkg
       - pull
       - --namespace=rpkg-init
-      - git-e5041a7290c88ac4ac34182c8b3d658ab97d353d
+      - git-95686470a1fd3a3ba726cce4c8f449f6bbe2b02a
     stdout: |
       apiVersion: config.kubernetes.io/v1
       items:
@@ -74,7 +74,7 @@ commands:
       - init
       - --namespace=rpkg-init
       - --repository=git
-      - --revision=v1
+      - --workspace=init-2
       - init-package
     stderr: "error: `init` cannot create a new revision for package \"init-package\" that already exists in repo \"git\"; make subsequent revisions using `copy`\n"
     exitCode: 1
diff --git a/e2e/testdata/porch/rpkg-lifecycle/config.yaml b/e2e/testdata/porch/rpkg-lifecycle/config.yaml
index d8ce786f..8383b257 100644
--- a/e2e/testdata/porch/rpkg-lifecycle/config.yaml
+++ b/e2e/testdata/porch/rpkg-lifecycle/config.yaml
@@ -12,91 +12,91 @@ commands:
       - init
       - --namespace=rpkg-lifecycle
       - --repository=git
-      - --revision=v1
+      - --workspace=lifecycle
       - lifecycle-package
-    stdout: "git-635026ba389a2dd5417b6e7577430c7ccff30f36 created\n"
+    stdout: "git-017a8366a5e0d9b35ae6dc489d4d3f68046d6034 created\n"
   - args:
       - alpha
       - rpkg
       - propose
       - --namespace=rpkg-lifecycle
-      - git-635026ba389a2dd5417b6e7577430c7ccff30f36
+      - git-017a8366a5e0d9b35ae6dc489d4d3f68046d6034
     stderr: |
-      git-635026ba389a2dd5417b6e7577430c7ccff30f36 proposed
+      git-017a8366a5e0d9b35ae6dc489d4d3f68046d6034 proposed
   - args:
       - alpha
       - rpkg
       - get
-      - git-635026ba389a2dd5417b6e7577430c7ccff30f36
+      - git-017a8366a5e0d9b35ae6dc489d4d3f68046d6034
       - --namespace=rpkg-lifecycle
       - --output=custom-columns=NAME:.metadata.name,PKG:.spec.packageName,REV:.spec.revision,LIFECYCLE:.spec.lifecycle
     stdout: |
-      NAME                                           PKG                 REV   LIFECYCLE
-      git-635026ba389a2dd5417b6e7577430c7ccff30f36   lifecycle-package   v1    Proposed
+      NAME                                           PKG                 REV      LIFECYCLE
+      git-017a8366a5e0d9b35ae6dc489d4d3f68046d6034   lifecycle-package   <none>   Proposed
   - args:
       - alpha
       - rpkg
       - reject
       - --namespace=rpkg-lifecycle
-      - git-635026ba389a2dd5417b6e7577430c7ccff30f36
+      - git-017a8366a5e0d9b35ae6dc489d4d3f68046d6034
     stderr: |
-      git-635026ba389a2dd5417b6e7577430c7ccff30f36 rejected
+      git-017a8366a5e0d9b35ae6dc489d4d3f68046d6034 rejected
   - args:
       - alpha
       - rpkg
       - get
-      - git-635026ba389a2dd5417b6e7577430c7ccff30f36
+      - git-017a8366a5e0d9b35ae6dc489d4d3f68046d6034
       - --namespace=rpkg-lifecycle
     stdout: |
-      NAME                                           PACKAGE             REVISION   LATEST   LIFECYCLE   REPOSITORY
-      git-635026ba389a2dd5417b6e7577430c7ccff30f36   lifecycle-package   v1         false    Draft       git
+      NAME                                           PACKAGE             WORKSPACENAME   REVISION   LATEST   LIFECYCLE   REPOSITORY
+      git-017a8366a5e0d9b35ae6dc489d4d3f68046d6034   lifecycle-package   lifecycle                  false    Draft       git
   - args:
       - alpha
       - rpkg
       - propose
       - --namespace=rpkg-lifecycle
-      - git-635026ba389a2dd5417b6e7577430c7ccff30f36
+      - git-017a8366a5e0d9b35ae6dc489d4d3f68046d6034
     stderr: |
-      git-635026ba389a2dd5417b6e7577430c7ccff30f36 proposed
+      git-017a8366a5e0d9b35ae6dc489d4d3f68046d6034 proposed
   - args:
       - alpha
       - rpkg
       - get
-      - git-635026ba389a2dd5417b6e7577430c7ccff30f36
+      - git-017a8366a5e0d9b35ae6dc489d4d3f68046d6034
       - --namespace=rpkg-lifecycle
     stdout: |
-      NAME                                           PACKAGE             REVISION   LATEST   LIFECYCLE   REPOSITORY
-      git-635026ba389a2dd5417b6e7577430c7ccff30f36   lifecycle-package   v1         false    Proposed    git
+      NAME                                           PACKAGE             WORKSPACENAME   REVISION   LATEST   LIFECYCLE   REPOSITORY
+      git-017a8366a5e0d9b35ae6dc489d4d3f68046d6034   lifecycle-package   lifecycle                  false    Proposed    git
   - args:
       - alpha
       - rpkg
       - approve
       - --namespace=rpkg-lifecycle
-      - git-635026ba389a2dd5417b6e7577430c7ccff30f36
+      - git-017a8366a5e0d9b35ae6dc489d4d3f68046d6034
     stderr: |
-      git-635026ba389a2dd5417b6e7577430c7ccff30f36 approved
+      git-017a8366a5e0d9b35ae6dc489d4d3f68046d6034 approved
   - args:
       - alpha
       - rpkg
       - get
-      - git-635026ba389a2dd5417b6e7577430c7ccff30f36
+      - git-017a8366a5e0d9b35ae6dc489d4d3f68046d6034
       - --namespace=rpkg-lifecycle
     stdout: |
-      NAME                                           PACKAGE             REVISION   LATEST   LIFECYCLE   REPOSITORY
-      git-635026ba389a2dd5417b6e7577430c7ccff30f36   lifecycle-package   v1         true     Published   git
+      NAME                                           PACKAGE             WORKSPACENAME   REVISION   LATEST   LIFECYCLE   REPOSITORY
+      git-017a8366a5e0d9b35ae6dc489d4d3f68046d6034   lifecycle-package   lifecycle       v1         true     Published   git
   - args:
       - alpha
       - rpkg
       - delete
-      - git-635026ba389a2dd5417b6e7577430c7ccff30f36
+      - git-017a8366a5e0d9b35ae6dc489d4d3f68046d6034
       - --namespace=rpkg-lifecycle
     stderr: |
-      git-635026ba389a2dd5417b6e7577430c7ccff30f36 deleted
+      git-017a8366a5e0d9b35ae6dc489d4d3f68046d6034 deleted
   - args:
       - alpha
       - rpkg
       - get
-      - git-635026ba389a2dd5417b6e7577430c7ccff30f36
+      - git-017a8366a5e0d9b35ae6dc489d4d3f68046d6034
       - --namespace=rpkg-lifecycle
-    stderr: "Error: the server could not find the requested resource (get packagerevisions.porch.kpt.dev git-635026ba389a2dd5417b6e7577430c7ccff30f36) \n"
+    stderr: "Error: the server could not find the requested resource (get packagerevisions.porch.kpt.dev git-017a8366a5e0d9b35ae6dc489d4d3f68046d6034) \n"
     exitCode: 1
diff --git a/e2e/testdata/porch/rpkg-push/config.yaml b/e2e/testdata/porch/rpkg-push/config.yaml
index cb823ae9..e9ba0c44 100644
--- a/e2e/testdata/porch/rpkg-push/config.yaml
+++ b/e2e/testdata/porch/rpkg-push/config.yaml
@@ -12,15 +12,15 @@ commands:
       - init
       - --namespace=rpkg-push
       - --repository=git
-      - --revision=v0
+      - --workspace=push
       - test-package
-    stdout: "git-951628068ed2aee1a62fc568131134c6ec95d0be created\n"
+    stdout: "git-efe3d01c68dfdcdd69114c9a7c65cce0d662a46f created\n"
   - args:
       - alpha
       - rpkg
       - pull
       - --namespace=rpkg-push
-      - git-951628068ed2aee1a62fc568131134c6ec95d0be
+      - git-efe3d01c68dfdcdd69114c9a7c65cce0d662a46f
     stdout: |
       apiVersion: config.kubernetes.io/v1
       items:
@@ -55,7 +55,7 @@ commands:
       - rpkg
       - push
       - --namespace=rpkg-push
-      - git-951628068ed2aee1a62fc568131134c6ec95d0be
+      - git-efe3d01c68dfdcdd69114c9a7c65cce0d662a46f
     stdin: |
       apiVersion: config.kubernetes.io/v1
       kind: ResourceList
@@ -99,7 +99,7 @@ commands:
       - rpkg
       - pull
       - --namespace=rpkg-push
-      - git-951628068ed2aee1a62fc568131134c6ec95d0be
+      - git-efe3d01c68dfdcdd69114c9a7c65cce0d662a46f
     stdout: |
       apiVersion: config.kubernetes.io/v1
       items:
diff --git a/e2e/testdata/porch/rpkg-update/config.yaml b/e2e/testdata/porch/rpkg-update/config.yaml
index 086d7790..2477322c 100644
--- a/e2e/testdata/porch/rpkg-update/config.yaml
+++ b/e2e/testdata/porch/rpkg-update/config.yaml
@@ -15,63 +15,63 @@ commands:
       - --directory=basens
       - --ref=basens/v1
       - --repository=git
-      - --revision=v0
+      - --workspace=update-1
       - basens-edit
-    stdout: "git-f1fbc2b72ee70b2f34e64c2630031d0560e6cf48 created\n"
+    stdout: "git-3f036055f7ba68706372cbe0c4b14d553794f7c4 created\n"
   - args:
       - alpha
       - rpkg
       - clone
       - --namespace=rpkg-update
-      - git-f1fbc2b72ee70b2f34e64c2630031d0560e6cf48
+      - git-3f036055f7ba68706372cbe0c4b14d553794f7c4
       - --directory=basens
       - --ref=basens/v1
       - --repository=git
-      - --revision=v0
+      - --workspace=update-2
       - basens-edit-clone
-    stdout: "git-542e4d9fd8ca2ee94edf43aadb3ec31dc2fbd3a0 created\n"
+    stdout: "git-7ab0219ace10c1081a8b40a6b97d5da58bdb62e0 created\n"
   - args:
       - alpha
       - rpkg
       - copy
       - --namespace=rpkg-update
-      - --revision=v2
-      - git-f1fbc2b72ee70b2f34e64c2630031d0560e6cf48
-    stdout: "git-894137a40c0792e88a9b844491a95a466b085ca1 created\n"
+      - --workspace=update-3
+      - git-3f036055f7ba68706372cbe0c4b14d553794f7c4
+    stdout: "git-7fcdd499f0790ac3bd8f805e3e5e00825641eb60 created\n"
   - args:
       - alpha
       - rpkg
       - propose
       - --namespace=rpkg-update
-      - git-894137a40c0792e88a9b844491a95a466b085ca1
-    stderr: "git-894137a40c0792e88a9b844491a95a466b085ca1 proposed\n"
+      - git-7fcdd499f0790ac3bd8f805e3e5e00825641eb60
+    stderr: "git-7fcdd499f0790ac3bd8f805e3e5e00825641eb60 proposed\n"
   - args:
       - alpha
       - rpkg
       - approve
       - --namespace=rpkg-update
-      - git-894137a40c0792e88a9b844491a95a466b085ca1
-    stderr: "git-894137a40c0792e88a9b844491a95a466b085ca1 approved\n"
+      - git-7fcdd499f0790ac3bd8f805e3e5e00825641eb60
+    stderr: "git-7fcdd499f0790ac3bd8f805e3e5e00825641eb60 approved\n"
   - args:
       - alpha
       - rpkg
       - get
       - --namespace=rpkg-update
     stdout: |
-      NAME                                           PACKAGE             REVISION   LATEST   LIFECYCLE   REPOSITORY
-      git-f1fbc2b72ee70b2f34e64c2630031d0560e6cf48   basens-edit         v0         false    Draft       git
-      git-894137a40c0792e88a9b844491a95a466b085ca1   basens-edit         v2         true     Published   git
-      git-542e4d9fd8ca2ee94edf43aadb3ec31dc2fbd3a0   basens-edit-clone   v0         false    Draft       git
+      NAME                                           PACKAGE             WORKSPACENAME   REVISION   LATEST   LIFECYCLE   REPOSITORY
+      git-3f036055f7ba68706372cbe0c4b14d553794f7c4   basens-edit         update-1                   false    Draft       git
+      git-7fcdd499f0790ac3bd8f805e3e5e00825641eb60   basens-edit         update-3        v1         true     Published   git
+      git-7ab0219ace10c1081a8b40a6b97d5da58bdb62e0   basens-edit-clone   update-2                   false    Draft       git
   - args:
       - alpha
       - rpkg
       - update
       - --namespace=rpkg-update
       - --discover=upstream
-      - git-542e4d9fd8ca2ee94edf43aadb3ec31dc2fbd3a0
+      - git-7ab0219ace10c1081a8b40a6b97d5da58bdb62e0
     stdout: |
       PACKAGE REVISION                               UPSTREAM REPOSITORY   UPSTREAM UPDATES
-      git-542e4d9fd8ca2ee94edf43aadb3ec31dc2fbd3a0   git                   v2
+      git-7ab0219ace10c1081a8b40a6b97d5da58bdb62e0   git                   v1
   - args:
       - alpha
       - rpkg
@@ -80,9 +80,9 @@ commands:
       - --discover=upstream
     stdout: |
       PACKAGE REVISION                               UPSTREAM REPOSITORY   UPSTREAM UPDATES
-      git-f1fbc2b72ee70b2f34e64c2630031d0560e6cf48                         No update available
-      git-894137a40c0792e88a9b844491a95a466b085ca1                         No update available
-      git-542e4d9fd8ca2ee94edf43aadb3ec31dc2fbd3a0   git                   v2
+      git-3f036055f7ba68706372cbe0c4b14d553794f7c4                         No update available
+      git-7fcdd499f0790ac3bd8f805e3e5e00825641eb60                         No update available
+      git-7ab0219ace10c1081a8b40a6b97d5da58bdb62e0   git                   v1
   - args:
       - alpha
       - rpkg
@@ -91,15 +91,15 @@ commands:
       - --discover=downstream
     stdout: |
       PACKAGE REVISION                               DOWNSTREAM PACKAGE                             DOWNSTREAM UPDATE
-      git-894137a40c0792e88a9b844491a95a466b085ca1   git-542e4d9fd8ca2ee94edf43aadb3ec31dc2fbd3a0   v0->v2
+      git-7fcdd499f0790ac3bd8f805e3e5e00825641eb60   git-7ab0219ace10c1081a8b40a6b97d5da58bdb62e0   (draft "update-1")->v1
   - args:
       - alpha
       - rpkg
       - update
       - --namespace=rpkg-update
-      - --revision=v2
-      - git-542e4d9fd8ca2ee94edf43aadb3ec31dc2fbd3a0
-    stdout: "git-542e4d9fd8ca2ee94edf43aadb3ec31dc2fbd3a0 updated\n"
+      - --revision=v1
+      - git-7ab0219ace10c1081a8b40a6b97d5da58bdb62e0
+    stdout: "git-7ab0219ace10c1081a8b40a6b97d5da58bdb62e0 updated\n"
   - args:
       - alpha
       - rpkg
@@ -108,6 +108,6 @@ commands:
       - --discover=upstream
     stdout: |
       PACKAGE REVISION                               UPSTREAM REPOSITORY   UPSTREAM UPDATES
-      git-f1fbc2b72ee70b2f34e64c2630031d0560e6cf48                         No update available
-      git-894137a40c0792e88a9b844491a95a466b085ca1                         No update available
-      git-542e4d9fd8ca2ee94edf43aadb3ec31dc2fbd3a0   git                   No update available
+      git-3f036055f7ba68706372cbe0c4b14d553794f7c4                         No update available
+      git-7fcdd499f0790ac3bd8f805e3e5e00825641eb60                         No update available
+      git-7ab0219ace10c1081a8b40a6b97d5da58bdb62e0   git                   No update available
diff --git a/porch/pkg/cache/cache_test.go b/porch/pkg/cache/cache_test.go
index 1933c687..f8408e51 100644
--- a/porch/pkg/cache/cache_test.go
+++ b/porch/pkg/cache/cache_test.go
@@ -85,8 +85,8 @@ func TestPublishedLatest(t *testing.T) {
 	_, cached := openRepositoryFromArchive(t, ctx, testPath, "nested")
 
 	revisions, err := cached.ListPackageRevisions(ctx, repository.ListPackageRevisionFilter{
-		Package:  "catalog/gcp/bucket",
-		Revision: "v2",
+		Package:       "catalog/gcp/bucket",
+		WorkspaceName: "v2",
 	})
 	if err != nil {
 		t.Fatalf("ListPackageRevisions failed: %v", err)
diff --git a/porch/pkg/git/git_test.go b/porch/pkg/git/git_test.go
index b8b4351c..cc16b5c7 100644
--- a/porch/pkg/git/git_test.go
+++ b/porch/pkg/git/git_test.go
@@ -128,7 +128,7 @@ func (g GitSuite) TestGitPackageRoundTrip(t *testing.T) {
 	const (
 		repositoryName = "roundtrip"
 		packageName    = "test-package"
-		revision       = "v123"
+		workspace      = "test-workspace"
 		namespace      = "default"
 		deployment     = true
 	)
@@ -162,7 +162,7 @@ func (g GitSuite) TestGitPackageRoundTrip(t *testing.T) {
 			},
 			Spec: v1alpha1.PackageRevisionSpec{
 				PackageName:    packageName,
-				Revision:       revision,
+				WorkspaceName:  workspace,
 				RepositoryName: repositoryName,
 			},
 			Status: v1alpha1.PackageRevisionStatus{},
@@ -195,9 +195,9 @@ func (g GitSuite) TestGitPackageRoundTrip(t *testing.T) {
 		}
 
 		original := findPackageRevision(t, revisions, repository.PackageRevisionKey{
-			Repository: repositoryName,
-			Package:    packageName,
-			Revision:   revision,
+			Repository:    repositoryName,
+			Package:       packageName,
+			WorkspaceName: workspace,
 		})
 
 		update, err := repo.UpdatePackageRevision(ctx, original)
@@ -209,7 +209,11 @@ func (g GitSuite) TestGitPackageRoundTrip(t *testing.T) {
 		}
 		approved, err := update.Close(ctx)
 		if err != nil {
-			t.Fatalf("Close() of %q, %q failed: %v", packageName, revision, err)
+			t.Fatalf("Close() of %q, %q failed: %v", packageName, workspace, err)
+		}
+		if approved.Key().Revision != "v1" {
+			t.Fatalf("UpdateLifecycle did not assign correct revision number; got %s, want v1",
+				approved.Key().Revision)
 		}
 
 		klog.Infof("approved revision %v", approved.KubeObjectName())
@@ -217,7 +221,7 @@ func (g GitSuite) TestGitPackageRoundTrip(t *testing.T) {
 
 	// Get the package again, the resources should match what we push
 	{
-		version := "v123"
+		version := "v1"
 
 		path := "test-package"
 		packageRevision, gitLock, err := repo.GetPackageRevision(ctx, version, path)
@@ -388,7 +392,7 @@ func (g GitSuite) TestListPackagesTrivial(t *testing.T) {
 		},
 		Spec: v1alpha1.PackageRevisionSpec{
 			PackageName:    "test-package",
-			Revision:       "v1",
+			WorkspaceName:  "test-workspace",
 			RepositoryName: repositoryName,
 			Lifecycle:      v1alpha1.PackageRevisionLifecycleDraft,
 		},
@@ -433,7 +437,7 @@ func (g GitSuite) TestListPackagesTrivial(t *testing.T) {
 		t.Fatalf("Failed to open git repository for verification: %v", err)
 	}
 	logRefs(t, verify, "Ref: ")
-	draftRefName := plumbing.NewBranchReferenceName("drafts/test-package/v1")
+	draftRefName := plumbing.NewBranchReferenceName("drafts/test-package/test-workspace")
 	if _, err = verify.Reference(draftRefName, true); err != nil {
 		t.Errorf("Failed to resolve %q references: %v", draftRefName, err)
 	}
@@ -476,7 +480,7 @@ func (g GitSuite) TestCreatePackageInTrivialRepository(t *testing.T) {
 		},
 		Spec: v1alpha1.PackageRevisionSpec{
 			PackageName:    "test-package",
-			Revision:       "v1",
+			WorkspaceName:  "test-workspace",
 			RepositoryName: repositoryName,
 			Lifecycle:      v1alpha1.PackageRevisionLifecycleDraft,
 		},
@@ -544,18 +548,18 @@ func (g GitSuite) TestListPackagesSimple(t *testing.T) {
 	}
 
 	want := map[repository.PackageRevisionKey]v1alpha1.PackageRevisionLifecycle{
-		{Repository: "simple", Package: "empty", Revision: "v1"}:   v1alpha1.PackageRevisionLifecyclePublished,
-		{Repository: "simple", Package: "basens", Revision: "v1"}:  v1alpha1.PackageRevisionLifecyclePublished,
-		{Repository: "simple", Package: "basens", Revision: "v2"}:  v1alpha1.PackageRevisionLifecyclePublished,
-		{Repository: "simple", Package: "istions", Revision: "v1"}: v1alpha1.PackageRevisionLifecyclePublished,
-		{Repository: "simple", Package: "istions", Revision: "v2"}: v1alpha1.PackageRevisionLifecyclePublished,
+		{Repository: "simple", Package: "empty", Revision: "v1", WorkspaceName: "v1"}:   v1alpha1.PackageRevisionLifecyclePublished,
+		{Repository: "simple", Package: "basens", Revision: "v1", WorkspaceName: "v1"}:  v1alpha1.PackageRevisionLifecyclePublished,
+		{Repository: "simple", Package: "basens", Revision: "v2", WorkspaceName: "v2"}:  v1alpha1.PackageRevisionLifecyclePublished,
+		{Repository: "simple", Package: "istions", Revision: "v1", WorkspaceName: "v1"}: v1alpha1.PackageRevisionLifecyclePublished,
+		{Repository: "simple", Package: "istions", Revision: "v2", WorkspaceName: "v2"}: v1alpha1.PackageRevisionLifecyclePublished,
 
 		// TODO: may want to filter these out, for example by including only those package
 		// revisions from main branch that differ in content (their tree hash) from another
 		// taged revision of the package.
-		{Repository: "simple", Package: "empty", Revision: g.branch}:   v1alpha1.PackageRevisionLifecyclePublished,
-		{Repository: "simple", Package: "basens", Revision: g.branch}:  v1alpha1.PackageRevisionLifecyclePublished,
-		{Repository: "simple", Package: "istions", Revision: g.branch}: v1alpha1.PackageRevisionLifecyclePublished,
+		{Repository: "simple", Package: "empty", Revision: g.branch, WorkspaceName: v1alpha1.WorkspaceName(g.branch)}:   v1alpha1.PackageRevisionLifecyclePublished,
+		{Repository: "simple", Package: "basens", Revision: g.branch, WorkspaceName: v1alpha1.WorkspaceName(g.branch)}:  v1alpha1.PackageRevisionLifecyclePublished,
+		{Repository: "simple", Package: "istions", Revision: g.branch, WorkspaceName: v1alpha1.WorkspaceName(g.branch)}: v1alpha1.PackageRevisionLifecyclePublished,
 	}
 
 	got := map[repository.PackageRevisionKey]v1alpha1.PackageRevisionLifecycle{}
@@ -565,9 +569,10 @@ func (g GitSuite) TestListPackagesSimple(t *testing.T) {
 			t.Errorf("didn't expect error, but got %v", err)
 		}
 		got[repository.PackageRevisionKey{
-			Repository: rev.Spec.RepositoryName,
-			Package:    rev.Spec.PackageName,
-			Revision:   rev.Spec.Revision,
+			Repository:    rev.Spec.RepositoryName,
+			Package:       rev.Spec.PackageName,
+			WorkspaceName: rev.Spec.WorkspaceName,
+			Revision:      rev.Spec.Revision,
 		}] = rev.Spec.Lifecycle
 	}
 
@@ -604,20 +609,20 @@ func (g GitSuite) TestListPackagesDrafts(t *testing.T) {
 	}
 
 	want := map[repository.PackageRevisionKey]v1alpha1.PackageRevisionLifecycle{
-		{Repository: "drafts", Package: "empty", Revision: "v1"}:   v1alpha1.PackageRevisionLifecyclePublished,
-		{Repository: "drafts", Package: "basens", Revision: "v1"}:  v1alpha1.PackageRevisionLifecyclePublished,
-		{Repository: "drafts", Package: "basens", Revision: "v2"}:  v1alpha1.PackageRevisionLifecyclePublished,
-		{Repository: "drafts", Package: "istions", Revision: "v1"}: v1alpha1.PackageRevisionLifecyclePublished,
-		{Repository: "drafts", Package: "istions", Revision: "v2"}: v1alpha1.PackageRevisionLifecyclePublished,
+		{Repository: "drafts", Package: "empty", Revision: "v1", WorkspaceName: "v1"}:   v1alpha1.PackageRevisionLifecyclePublished,
+		{Repository: "drafts", Package: "basens", Revision: "v1", WorkspaceName: "v1"}:  v1alpha1.PackageRevisionLifecyclePublished,
+		{Repository: "drafts", Package: "basens", Revision: "v2", WorkspaceName: "v2"}:  v1alpha1.PackageRevisionLifecyclePublished,
+		{Repository: "drafts", Package: "istions", Revision: "v1", WorkspaceName: "v1"}: v1alpha1.PackageRevisionLifecyclePublished,
+		{Repository: "drafts", Package: "istions", Revision: "v2", WorkspaceName: "v2"}: v1alpha1.PackageRevisionLifecyclePublished,
 
-		{Repository: "drafts", Package: "bucket", Revision: "v1"}:           v1alpha1.PackageRevisionLifecycleDraft,
-		{Repository: "drafts", Package: "none", Revision: "v1"}:             v1alpha1.PackageRevisionLifecycleDraft,
-		{Repository: "drafts", Package: "pkg-with-history", Revision: "v1"}: v1alpha1.PackageRevisionLifecycleDraft,
+		{Repository: "drafts", Package: "bucket", WorkspaceName: "v1"}:           v1alpha1.PackageRevisionLifecycleDraft,
+		{Repository: "drafts", Package: "none", WorkspaceName: "v1"}:             v1alpha1.PackageRevisionLifecycleDraft,
+		{Repository: "drafts", Package: "pkg-with-history", WorkspaceName: "v1"}: v1alpha1.PackageRevisionLifecycleDraft,
 
 		// TODO: filter main branch out? see above
-		{Repository: "drafts", Package: "basens", Revision: g.branch}:  v1alpha1.PackageRevisionLifecyclePublished,
-		{Repository: "drafts", Package: "empty", Revision: g.branch}:   v1alpha1.PackageRevisionLifecyclePublished,
-		{Repository: "drafts", Package: "istions", Revision: g.branch}: v1alpha1.PackageRevisionLifecyclePublished,
+		{Repository: "drafts", Package: "basens", WorkspaceName: v1alpha1.WorkspaceName(g.branch), Revision: g.branch}:  v1alpha1.PackageRevisionLifecyclePublished,
+		{Repository: "drafts", Package: "empty", WorkspaceName: v1alpha1.WorkspaceName(g.branch), Revision: g.branch}:   v1alpha1.PackageRevisionLifecyclePublished,
+		{Repository: "drafts", Package: "istions", WorkspaceName: v1alpha1.WorkspaceName(g.branch), Revision: g.branch}: v1alpha1.PackageRevisionLifecyclePublished,
 	}
 
 	got := map[repository.PackageRevisionKey]v1alpha1.PackageRevisionLifecycle{}
@@ -627,9 +632,10 @@ func (g GitSuite) TestListPackagesDrafts(t *testing.T) {
 			t.Errorf("didn't expect error, but got %v", err)
 		}
 		got[repository.PackageRevisionKey{
-			Repository: rev.Spec.RepositoryName,
-			Package:    rev.Spec.PackageName,
-			Revision:   rev.Spec.Revision,
+			Repository:    rev.Spec.RepositoryName,
+			Package:       rev.Spec.PackageName,
+			Revision:      rev.Spec.Revision,
+			WorkspaceName: rev.Spec.WorkspaceName,
 		}] = rev.Spec.Lifecycle
 	}
 
@@ -666,9 +672,9 @@ func (g GitSuite) TestApproveDraft(t *testing.T) {
 	}
 
 	bucket := findPackageRevision(t, revisions, repository.PackageRevisionKey{
-		Repository: repositoryName,
-		Package:    "bucket",
-		Revision:   "v1",
+		Repository:    repositoryName,
+		Package:       "bucket",
+		WorkspaceName: "v1",
 	})
 
 	// Before Update; Check server references. Draft must exist, final not.
@@ -728,9 +734,9 @@ func (g GitSuite) TestApproveDraftWithHistory(t *testing.T) {
 	}
 
 	bucket := findPackageRevision(t, revisions, repository.PackageRevisionKey{
-		Repository: repositoryName,
-		Package:    "pkg-with-history",
-		Revision:   "v1",
+		Repository:    repositoryName,
+		Package:       "pkg-with-history",
+		WorkspaceName: "v1",
 	})
 
 	// Before Update; Check server references. Draft must exist, final not.
@@ -809,7 +815,8 @@ func (g GitSuite) TestDeletePackages(t *testing.T) {
 		if err != nil {
 			t.Fatalf("didn't expect error, but got %v", err)
 		}
-		name := repository.PackageRevisionKey{Repository: pr.Spec.RepositoryName, Package: pr.Spec.PackageName, Revision: pr.Spec.Revision}
+		name := repository.PackageRevisionKey{Repository: pr.Spec.RepositoryName, Package: pr.Spec.PackageName,
+			Revision: pr.Spec.Revision, WorkspaceName: pr.Spec.WorkspaceName}
 
 		if rn, ok := wantDeletedRefs[name]; ok {
 			// Verify the reference still exists
@@ -882,9 +889,10 @@ func (g GitSuite) TestRefreshRepo(t *testing.T) {
 	)
 
 	newPackageName := repository.PackageRevisionKey{
-		Repository: "refresh",
-		Package:    "newpkg",
-		Revision:   "v3",
+		Repository:    "refresh",
+		Package:       "newpkg",
+		Revision:      "v3",
+		WorkspaceName: "v3",
 	}
 
 	ctx := context.Background()
@@ -901,7 +909,8 @@ func (g GitSuite) TestRefreshRepo(t *testing.T) {
 	}
 
 	// Confirm we listed some package(s)
-	findPackageRevision(t, all, repository.PackageRevisionKey{Repository: "refresh", Package: "basens", Revision: "v2"})
+	findPackageRevision(t, all, repository.PackageRevisionKey{Repository: "refresh", Package: "basens",
+		Revision: "v2", WorkspaceName: "v2"})
 	packageMustNotExist(t, all, newPackageName)
 
 	// Create package in the upstream repository
@@ -983,31 +992,31 @@ func (g GitSuite) TestPruneRemotes(t *testing.T) {
 	}{
 		{
 			ref: "refs/heads/drafts/bucket/v1",
-			pkg: repository.PackageRevisionKey{Repository: "prune", Package: "bucket", Revision: "v1"},
+			pkg: repository.PackageRevisionKey{Repository: "prune", Package: "bucket", WorkspaceName: "v1"},
 		},
 		{
 			ref: "refs/heads/drafts/none/v1",
-			pkg: repository.PackageRevisionKey{Repository: "prune", Package: "none", Revision: "v1"},
+			pkg: repository.PackageRevisionKey{Repository: "prune", Package: "none", WorkspaceName: "v1"},
 		},
 		{
 			ref: "refs/tags/basens/v1",
-			pkg: repository.PackageRevisionKey{Repository: "prune", Package: "basens", Revision: "v1"},
+			pkg: repository.PackageRevisionKey{Repository: "prune", Package: "basens", Revision: "v1", WorkspaceName: "v1"},
 		},
 		{
 			ref: "refs/tags/basens/v2",
-			pkg: repository.PackageRevisionKey{Repository: "prune", Package: "basens", Revision: "v2"},
+			pkg: repository.PackageRevisionKey{Repository: "prune", Package: "basens", Revision: "v2", WorkspaceName: "v2"},
 		},
 		{
 			ref: "refs/tags/empty/v1",
-			pkg: repository.PackageRevisionKey{Repository: "prune", Package: "empty", Revision: "v1"},
+			pkg: repository.PackageRevisionKey{Repository: "prune", Package: "empty", Revision: "v1", WorkspaceName: "v1"},
 		},
 		{
 			ref: "refs/tags/istions/v1",
-			pkg: repository.PackageRevisionKey{Repository: "prune", Package: "istions", Revision: "v1"},
+			pkg: repository.PackageRevisionKey{Repository: "prune", Package: "istions", Revision: "v1", WorkspaceName: "v1"},
 		},
 		{
 			ref: "refs/tags/istions/v2",
-			pkg: repository.PackageRevisionKey{Repository: "prune", Package: "istions", Revision: "v2"},
+			pkg: repository.PackageRevisionKey{Repository: "prune", Package: "istions", Revision: "v2", WorkspaceName: "v2"},
 		},
 	} {
 		repositoryMustHavePackageRevision(t, git, pair.pkg)
@@ -1080,7 +1089,11 @@ func (g GitSuite) TestNested(t *testing.T) {
 			// to match the above simplified package discovery algo.
 			continue
 		}
-		got[fmt.Sprintf("%s/%s", rev.Spec.PackageName, rev.Spec.Revision)] = rev.Spec.Lifecycle
+		if rev.Spec.Lifecycle == v1alpha1.PackageRevisionLifecyclePublished {
+			got[fmt.Sprintf("%s/%s", rev.Spec.PackageName, rev.Spec.Revision)] = rev.Spec.Lifecycle
+		} else {
+			got[fmt.Sprintf("%s/%s", rev.Spec.PackageName, rev.Spec.WorkspaceName)] = rev.Spec.Lifecycle
+		}
 	}
 
 	if !cmp.Equal(want, got) {
@@ -1092,7 +1105,11 @@ func createPackageRevisionMap(revisions []repository.PackageRevision) map[string
 	result := map[string]bool{}
 	for _, pr := range revisions {
 		key := pr.Key()
-		result[fmt.Sprintf("%s/%s", key.Package, key.Revision)] = true
+		if key.WorkspaceName != "" {
+			result[fmt.Sprintf("%s/%s", key.Package, key.WorkspaceName)] = true
+		} else {
+			result[fmt.Sprintf("%s/%s", key.Package, key.Revision)] = true
+		}
 	}
 	return result
 }
@@ -1171,31 +1188,35 @@ func (g GitSuite) TestAuthor(t *testing.T) {
 
 	testCases := map[string]struct {
 		pkg       string
+		workspace string
 		revision  string
 		author    string
 		timestamp time.Time
 	}{
 		"draft packagerevision does not have publishing info in status": {
 			pkg:       "draft-pkg",
-			revision:  "v1",
+			workspace: "v1",
 			author:    "",
 			timestamp: time.Time{},
 		},
 		"published packagerevision on tag": {
 			pkg:       "pkg-with-anno",
 			revision:  "v1",
+			workspace: "v1",
 			author:    "pkg-with-anno-author@example.com",
 			timestamp: time.Date(2022, time.August, 26, 22, 47, 35, 0, time.UTC),
 		},
 		"published packagerevision on main without commit annotations": {
 			pkg:       "pkg-without-anno",
 			revision:  g.branch,
+			workspace: g.branch,
 			author:    "",
 			timestamp: time.Time{},
 		},
 		"published packagerevision on main with commit annotations": {
 			pkg:       "pkg-with-anno",
 			revision:  g.branch,
+			workspace: g.branch,
 			author:    "pkg-with-anno-author@example.com",
 			timestamp: time.Date(2022, time.August, 26, 22, 47, 35, 0, time.UTC),
 		},
@@ -1229,9 +1250,10 @@ func (g GitSuite) TestAuthor(t *testing.T) {
 
 			_ = revisions
 			draftPkg := findPackageRevision(t, revisions, repository.PackageRevisionKey{
-				Repository: repositoryName,
-				Package:    tc.pkg,
-				Revision:   tc.revision,
+				Repository:    repositoryName,
+				Package:       tc.pkg,
+				WorkspaceName: v1alpha1.WorkspaceName(tc.workspace),
+				Revision:      tc.revision,
 			})
 			rev, err := draftPkg.GetPackageRevision(ctx)
 			if err != nil {
diff --git a/porch/pkg/git/package_test.go b/porch/pkg/git/package_test.go
index e8e59e6f..04718ceb 100644
--- a/porch/pkg/git/package_test.go
+++ b/porch/pkg/git/package_test.go
@@ -54,15 +54,15 @@ func (g GitSuite) TestLock(t *testing.T) {
 	}
 
 	wantRefs := map[repository.PackageRevisionKey]string{
-		{Repository: repositoryName, Package: "empty", Revision: "v1"}:   "empty/v1",
-		{Repository: repositoryName, Package: "basens", Revision: "v1"}:  "basens/v1",
-		{Repository: repositoryName, Package: "basens", Revision: "v2"}:  "basens/v2",
-		{Repository: repositoryName, Package: "istions", Revision: "v1"}: "istions/v1",
-		{Repository: repositoryName, Package: "istions", Revision: "v2"}: "istions/v2",
-
-		{Repository: repositoryName, Package: "basens", Revision: g.branch}:  g.branch,
-		{Repository: repositoryName, Package: "empty", Revision: g.branch}:   g.branch,
-		{Repository: repositoryName, Package: "istions", Revision: g.branch}: g.branch,
+		{Repository: repositoryName, Package: "empty", Revision: "v1", WorkspaceName: "v1"}:   "empty/v1",
+		{Repository: repositoryName, Package: "basens", Revision: "v1", WorkspaceName: "v1"}:  "basens/v1",
+		{Repository: repositoryName, Package: "basens", Revision: "v2", WorkspaceName: "v2"}:  "basens/v2",
+		{Repository: repositoryName, Package: "istions", Revision: "v1", WorkspaceName: "v1"}: "istions/v1",
+		{Repository: repositoryName, Package: "istions", Revision: "v2", WorkspaceName: "v2"}: "istions/v2",
+
+		{Repository: repositoryName, Package: "basens", Revision: g.branch, WorkspaceName: v1alpha1.WorkspaceName(g.branch)}:  g.branch,
+		{Repository: repositoryName, Package: "empty", Revision: g.branch, WorkspaceName: v1alpha1.WorkspaceName(g.branch)}:   g.branch,
+		{Repository: repositoryName, Package: "istions", Revision: g.branch, WorkspaceName: v1alpha1.WorkspaceName(g.branch)}: g.branch,
 	}
 
 	for _, rev := range revisions {
diff --git a/porch/pkg/repository/util_test.go b/porch/pkg/repository/util_test.go
new file mode 100644
index 00000000..511bd3ae
--- /dev/null
+++ b/porch/pkg/repository/util_test.go
@@ -0,0 +1,77 @@
+// Copyright 2022 Google LLC
+//
+// 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 repository
+
+import (
+	"fmt"
+	"testing"
+
+	v1alpha1 "github.com/GoogleContainerTools/kpt/porch/api/porch/v1alpha1"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestValidateWorkspaceName(t *testing.T) {
+	testCases := map[string]struct {
+		workspace   string
+		expectedErr error
+	}{
+		"empty string": {
+			workspace:   "",
+			expectedErr: fmt.Errorf("workspaceName %q must be at least 1 and at most 63 characters long", ""),
+		},
+
+		"64 characters long": {
+			workspace:   "abcedfhglaasdkfuaweoihfjghldhsgufhgaakjsdhaflkasdjflksadjfsalsdk",
+			expectedErr: fmt.Errorf("workspaceName %q must be at least 1 and at most 63 characters long", "abcedfhglaasdkfuaweoihfjghldhsgufhgaakjsdhaflkasdjflksadjfsalsdk"),
+		},
+
+		"63 characters long": {
+			workspace:   "abcedfhglaasdkfuaweoihfjghldhsgufhgaakjsdhaflkasdjflksadjfsalsk",
+			expectedErr: nil,
+		},
+
+		"starts with -": {
+			workspace:   "-hello",
+			expectedErr: fmt.Errorf("workspaceName %q must start and end with an alphanumeric character", "-hello"),
+		},
+
+		"ends with -": {
+			workspace:   "hello-",
+			expectedErr: fmt.Errorf("workspaceName %q must start and end with an alphanumeric character", "hello-"),
+		},
+
+		"has - in the middle": {
+			workspace:   "hel-lo-wor-ld",
+			expectedErr: nil,
+		},
+
+		"has uppercase alphanumeric characters": {
+			workspace:   "hElLo",
+			expectedErr: fmt.Errorf("workspaceName %q must be comprised only of lowercase alphanumeric characters and '-'", "hElLo"),
+		},
+
+		"has other characters": {
+			workspace:   "hel lo",
+			expectedErr: fmt.Errorf("workspaceName %q must be comprised only of lowercase alphanumeric characters and '-'", "hel lo"),
+		},
+	}
+
+	for tn, tc := range testCases {
+		t.Run(tn, func(t *testing.T) {
+			got := ValidateWorkspaceName(v1alpha1.WorkspaceName(tc.workspace))
+			assert.Equal(t, tc.expectedErr, got)
+		})
+	}
+}
diff --git a/porch/test/e2e/e2e_test.go b/porch/test/e2e/e2e_test.go
index ca15de64..9aaca03b 100644
--- a/porch/test/e2e/e2e_test.go
+++ b/porch/test/e2e/e2e_test.go
@@ -111,7 +111,7 @@ func (t *PorchSuite) TestGitRepository(ctx context.Context) {
 		},
 		Spec: porchapi.PackageRevisionSpec{
 			PackageName:    "test-bucket",
-			Revision:       "v1",
+			WorkspaceName:  "workspace",
 			RepositoryName: "git",
 			Tasks: []porchapi.Task{
 				{
@@ -191,7 +191,7 @@ func (t *PorchSuite) TestCloneFromUpstream(ctx context.Context) {
 		},
 		Spec: porchapi.PackageRevisionSpec{
 			PackageName:    "istions",
-			Revision:       "v1",
+			WorkspaceName:  "test-workspace",
 			RepositoryName: "downstream",
 			Tasks: []porchapi.Task{
 				{
@@ -267,6 +267,7 @@ func (t *PorchSuite) TestInitEmptyPackage(ctx context.Context) {
 		repository  = "git"
 		packageName = "empty-package"
 		revision    = "v1"
+		workspace   = "test-workspace"
 		description = "empty-package description"
 	)
 
@@ -284,7 +285,7 @@ func (t *PorchSuite) TestInitEmptyPackage(ctx context.Context) {
 		},
 		Spec: porchapi.PackageRevisionSpec{
 			PackageName:    "empty-package",
-			Revision:       "v1",
+			WorkspaceName:  workspace,
 			RepositoryName: repository,
 		},
 	}
@@ -313,6 +314,7 @@ func (t *PorchSuite) TestInitTaskPackage(ctx context.Context) {
 		repository  = "git"
 		packageName = "new-package"
 		revision    = "v1"
+		workspace   = "test-workspace"
 		description = "New Package"
 		site        = "https://kpt.dev/new-package"
 	)
@@ -332,7 +334,7 @@ func (t *PorchSuite) TestInitTaskPackage(ctx context.Context) {
 		},
 		Spec: porchapi.PackageRevisionSpec{
 			PackageName:    "new-package",
-			Revision:       "v1",
+			WorkspaceName:  workspace,
 			RepositoryName: repository,
 			Tasks: []porchapi.Task{
 				{
@@ -372,6 +374,7 @@ func (t *PorchSuite) TestCloneIntoDeploymentRepository(ctx context.Context) {
 	const downstreamRepository = "deployment"
 	const downstreamPackage = "istions"
 	const downstreamRevision = "v2"
+	const downstreamWorkspace = "test-workspace"
 
 	// Register the deployment repository
 	t.registerMainGitRepositoryF(ctx, downstreamRepository, withDeployment())
@@ -382,9 +385,10 @@ func (t *PorchSuite) TestCloneIntoDeploymentRepository(ctx context.Context) {
 	var upstreamPackages porchapi.PackageRevisionList
 	t.ListE(ctx, &upstreamPackages, client.InNamespace(t.namespace))
 	upstreamPackage := MustFindPackageRevision(t.T, &upstreamPackages, repository.PackageRevisionKey{
-		Repository: "test-blueprints",
-		Package:    "basens",
-		Revision:   "v1",
+		Repository:    "test-blueprints",
+		Package:       "basens",
+		Revision:      "v1",
+		WorkspaceName: "v1",
 	})
 
 	// Create PackageRevision from upstream repo
@@ -398,7 +402,7 @@ func (t *PorchSuite) TestCloneIntoDeploymentRepository(ctx context.Context) {
 		},
 		Spec: porchapi.PackageRevisionSpec{
 			PackageName:    downstreamPackage,
-			Revision:       downstreamRevision,
+			WorkspaceName:  downstreamWorkspace,
 			RepositoryName: downstreamRepository,
 			Tasks: []porchapi.Task{
 				{
@@ -483,7 +487,7 @@ func (t *PorchSuite) TestUpdateResources(ctx context.Context) {
 	const (
 		repository  = "re-render-test"
 		packageName = "simple-package"
-		revision    = "v3"
+		description = "description"
 	)
 
 	t.registerMainGitRepositoryF(ctx, repository)
@@ -499,7 +503,7 @@ func (t *PorchSuite) TestUpdateResources(ctx context.Context) {
 		},
 		Spec: porchapi.PackageRevisionSpec{
 			PackageName:    packageName,
-			Revision:       revision,
+			WorkspaceName:  description,
 			RepositoryName: repository,
 		},
 	}
@@ -603,9 +607,9 @@ func (t *PorchSuite) TestPublicGitRepository(ctx context.Context) {
 
 func (t *PorchSuite) TestProposeApprove(ctx context.Context) {
 	const (
-		repository      = "lifecycle"
-		packageName     = "test-package"
-		packageRevision = "v1"
+		repository  = "lifecycle"
+		packageName = "test-package"
+		workspace   = "workspace"
 	)
 
 	// Register the repository
@@ -622,7 +626,7 @@ func (t *PorchSuite) TestProposeApprove(ctx context.Context) {
 		},
 		Spec: porchapi.PackageRevisionSpec{
 			PackageName:    packageName,
-			Revision:       packageRevision,
+			WorkspaceName:  workspace,
 			RepositoryName: repository,
 			Tasks: []porchapi.Task{
 				{
@@ -666,6 +670,11 @@ func (t *PorchSuite) TestProposeApprove(ctx context.Context) {
 	if got, want := approved.Spec.Lifecycle, porchapi.PackageRevisionLifecyclePublished; got != want {
 		t.Fatalf("Approved package lifecycle value: got %s, want %s", got, want)
 	}
+
+	// Check its revision number
+	if got, want := approved.Spec.Revision, "v1"; got != want {
+		t.Fatalf("Approved package revision value: got %s, want %s", got, want)
+	}
 }
 
 func (t *PorchSuite) TestDeleteDraft(ctx context.Context) {
@@ -673,13 +682,14 @@ func (t *PorchSuite) TestDeleteDraft(ctx context.Context) {
 		repository  = "delete-draft"
 		packageName = "test-delete-draft"
 		revision    = "v1"
+		workspace   = "test-workspace"
 	)
 
 	// Register the repository
 	t.registerMainGitRepositoryF(ctx, repository)
 
 	// Create a draft package
-	created := t.createPackageDraftF(ctx, repository, packageName, revision)
+	created := t.createPackageDraftF(ctx, repository, packageName, workspace)
 
 	// Check the package exists
 	var draft porchapi.PackageRevision
@@ -701,13 +711,14 @@ func (t *PorchSuite) TestDeleteProposed(ctx context.Context) {
 		repository  = "delete-proposed"
 		packageName = "test-delete-proposed"
 		revision    = "v1"
+		workspace   = "workspace"
 	)
 
 	// Register the repository
 	t.registerMainGitRepositoryF(ctx, repository)
 
 	// Create a draft package
-	created := t.createPackageDraftF(ctx, repository, packageName, revision)
+	created := t.createPackageDraftF(ctx, repository, packageName, workspace)
 
 	// Check the package exists
 	var pkg porchapi.PackageRevision
@@ -733,13 +744,14 @@ func (t *PorchSuite) TestDeleteFinal(ctx context.Context) {
 		repository  = "delete-final"
 		packageName = "test-delete-final"
 		revision    = "v1"
+		workspace   = "workspace"
 	)
 
 	// Register the repository
 	t.registerMainGitRepositoryF(ctx, repository)
 
 	// Create a draft package
-	created := t.createPackageDraftF(ctx, repository, packageName, revision)
+	created := t.createPackageDraftF(ctx, repository, packageName, workspace)
 
 	// Check the package exists
 	var pkg porchapi.PackageRevision
@@ -770,13 +782,14 @@ func (t *PorchSuite) TestDeleteAndRecreate(ctx context.Context) {
 		repository  = "delete-and-recreate"
 		packageName = "test-delete-and-recreate"
 		revision    = "v1"
+		workspace   = "work"
 	)
 
 	// Register the repository
 	t.registerMainGitRepositoryF(ctx, repository)
 
 	// Create a draft package
-	created := t.createPackageDraftF(ctx, repository, packageName, revision)
+	created := t.createPackageDraftF(ctx, repository, packageName, workspace)
 
 	// Check the package exists
 	var pkg porchapi.PackageRevision
@@ -786,6 +799,10 @@ func (t *PorchSuite) TestDeleteAndRecreate(ctx context.Context) {
 	pkg.Spec.Lifecycle = porchapi.PackageRevisionLifecycleProposed
 	t.UpdateF(ctx, &pkg)
 
+	// Assign a revision number
+	pkg.Spec.Revision = "v1"
+	t.UpdateF(ctx, &pkg)
+
 	pkg.Spec.Lifecycle = porchapi.PackageRevisionLifecyclePublished
 	t.UpdateApprovalF(ctx, &pkg, metav1.UpdateOptions{})
 
@@ -801,8 +818,8 @@ func (t *PorchSuite) TestDeleteAndRecreate(ctx context.Context) {
 
 	t.mustNotExist(ctx, &pkg)
 
-	// Recreate the package with the same name and revision
-	created = t.createPackageDraftF(ctx, repository, packageName, revision)
+	// Recreate the package with the same name and workspace
+	created = t.createPackageDraftF(ctx, repository, packageName, workspace)
 
 	// Check the package exists
 	t.mustExist(ctx, client.ObjectKey{Namespace: t.namespace, Name: created.Name}, &pkg)
@@ -825,6 +842,7 @@ func (t *PorchSuite) TestCloneLeadingSlash(ctx context.Context) {
 		repository  = "clone-ls"
 		packageName = "test-clone-ls"
 		revision    = "v1"
+		workspace   = "workspace"
 	)
 
 	t.registerMainGitRepositoryF(ctx, repository)
@@ -840,7 +858,7 @@ func (t *PorchSuite) TestCloneLeadingSlash(ctx context.Context) {
 		},
 		Spec: porchapi.PackageRevisionSpec{
 			PackageName:    packageName,
-			Revision:       revision,
+			WorkspaceName:  workspace,
 			RepositoryName: repository,
 			Tasks: []porchapi.Task{
 				{
@@ -894,7 +912,7 @@ func (t *PorchSuite) TestPackageUpdate(ctx context.Context) {
 		},
 		Spec: porchapi.PackageRevisionSpec{
 			PackageName:    "testns",
-			Revision:       "v1",
+			WorkspaceName:  "test-workspace",
 			RepositoryName: gitRepository,
 			Tasks: []porchapi.Task{
 				{
@@ -1000,7 +1018,7 @@ func (t *PorchSuite) TestBuiltinFunctionEvaluator(ctx context.Context) {
 		},
 		Spec: porchapi.PackageRevisionSpec{
 			PackageName:    "test-builtin-fn-bucket",
-			Revision:       "v1",
+			WorkspaceName:  "test-workspace",
 			RepositoryName: "git-builtin-fn",
 			Tasks: []porchapi.Task{
 				{
@@ -1078,7 +1096,7 @@ func (t *PorchSuite) TestExecFunctionEvaluator(ctx context.Context) {
 		},
 		Spec: porchapi.PackageRevisionSpec{
 			PackageName:    "test-fn-bucket",
-			Revision:       "v1",
+			WorkspaceName:  "test-workspace",
 			RepositoryName: "git-fn",
 			Tasks: []porchapi.Task{
 				{
@@ -1158,7 +1176,7 @@ func (t *PorchSuite) TestPodFunctionEvaluatorWithDistrolessImage(ctx context.Con
 		},
 		Spec: porchapi.PackageRevisionSpec{
 			PackageName:    "test-fn-redis-bucket",
-			Revision:       "v1",
+			WorkspaceName:  "test-description",
 			RepositoryName: "git-fn-distroless",
 			Tasks: []porchapi.Task{
 				{
@@ -1245,7 +1263,7 @@ func (t *PorchSuite) TestPodEvaluator(ctx context.Context) {
 		},
 		Spec: porchapi.PackageRevisionSpec{
 			PackageName:    "test-fn-pod-hierarchy",
-			Revision:       "v1",
+			WorkspaceName:  "workspace-1",
 			RepositoryName: "git-fn-pod",
 			Tasks: []porchapi.Task{
 				{
@@ -1324,7 +1342,7 @@ func (t *PorchSuite) TestPodEvaluator(ctx context.Context) {
 		},
 		Spec: porchapi.PackageRevisionSpec{
 			PackageName:    "test-fn-pod-hierarchy",
-			Revision:       "v2",
+			WorkspaceName:  "workspace-2",
 			RepositoryName: "git-fn-pod",
 			Tasks: []porchapi.Task{
 				{
@@ -1400,7 +1418,7 @@ func (t *PorchSuite) TestPodEvaluatorWithFailure(ctx context.Context) {
 		},
 		Spec: porchapi.PackageRevisionSpec{
 			PackageName:    "test-fn-pod-bucket",
-			Revision:       "v1",
+			WorkspaceName:  "workspace",
 			RepositoryName: "git-fn-pod-failure",
 			Tasks: []porchapi.Task{
 				{
@@ -1531,7 +1549,7 @@ func (t *PorchSuite) TestNewPackageRevisionLabels(ctx context.Context) {
 		},
 		Spec: porchapi.PackageRevisionSpec{
 			PackageName:    "new-package",
-			Revision:       "v1",
+			WorkspaceName:  "workspace",
 			RepositoryName: repository,
 			Tasks: []porchapi.Task{
 				{
@@ -1585,6 +1603,7 @@ func (t *PorchSuite) TestNewPackageRevisionLabels(ctx context.Context) {
 	delete(pr.ObjectMeta.Labels, labelKey1)
 	pr.ObjectMeta.Labels[labelKey2] = labelVal2
 	delete(pr.ObjectMeta.Annotations, annoKey2)
+	pr.Spec.Revision = "v1"
 	t.UpdateF(ctx, &pr)
 	t.validateLabelsAndAnnos(ctx, pr.Name,
 		map[string]string{
@@ -1608,7 +1627,7 @@ func (t *PorchSuite) TestNewPackageRevisionLabels(ctx context.Context) {
 		},
 		Spec: porchapi.PackageRevisionSpec{
 			PackageName:    "cloned-package",
-			Revision:       "v1",
+			WorkspaceName:  "workspace",
 			RepositoryName: repository,
 			Tasks: []porchapi.Task{
 				{
@@ -1764,7 +1783,7 @@ func (t *PorchSuite) registerMainGitRepositoryF(ctx context.Context, name string
 			Namespace: t.namespace,
 		},
 		Spec: configapi.RepositorySpec{
-			Description: "Porch Test Repository Description",
+			Description: "Porch Test Repository WorkspaceName",
 			Type:        configapi.RepositoryTypeGit,
 			Content:     configapi.RepositoryContentPackage,
 			Git: &configapi.GitRepository{
@@ -1817,7 +1836,7 @@ func withContent(content configapi.RepositoryContent) repositoryOption {
 }
 
 // Creates an empty package draft by initializing an empty package
-func (t *PorchSuite) createPackageDraftF(ctx context.Context, repository, name, revision string) *porchapi.PackageRevision {
+func (t *PorchSuite) createPackageDraftF(ctx context.Context, repository, name, workspace string) *porchapi.PackageRevision {
 	pr := &porchapi.PackageRevision{
 		TypeMeta: metav1.TypeMeta{
 			Kind:       "PackageRevision",
@@ -1828,7 +1847,7 @@ func (t *PorchSuite) createPackageDraftF(ctx context.Context, repository, name,
 		},
 		Spec: porchapi.PackageRevisionSpec{
 			PackageName:    name,
-			Revision:       revision,
+			WorkspaceName:  porchapi.WorkspaceName(workspace),
 			RepositoryName: repository,
 			Tasks: []porchapi.Task{
 				{
diff --git a/porch/test/e2e/update_test.go b/porch/test/e2e/update_test.go
index 47b2abf5..8db0fe4b 100644
--- a/porch/test/e2e/update_test.go
+++ b/porch/test/e2e/update_test.go
@@ -50,7 +50,7 @@ func (t *PorchSuite) TestPackageUpdateRecloneAndReplay(ctx context.Context) {
 		},
 		Spec: porchapi.PackageRevisionSpec{
 			PackageName:    "testRecloneAndReplay",
-			Revision:       "v1",
+			WorkspaceName:  "testdescr",
 			RepositoryName: gitRepository,
 			Tasks: []porchapi.Task{
 				{