Repository: getporter/porter
Pull request #1537 (2021-04-08 17:02:57)

fix(archive.go): repeatable shasum on archive
# What does this change
* Adds logic to ensure identical archive shasums are produced, provided the same bundle is used

Uses the approach of zero-ing out the timestamps for all archive files to achieve this.  As seen in https://unix.stackexchange.com/questions/346789/compressing-two-identical-folders-give-different-result and suggested by @sebumd in the original issue https://github.com/getporter/porter/issues/1526

# What issue does it fix
Closes https://github.com/getporter/porter/issues/1526

# Notes for the reviewer

* The original timestamps for all files in an expanded archive will no longer be preserved
* For testing, I updated the archive integration test to _not_ use a randomized bundle name and reference, so that we get a deterministic archive hash for matching on

# Checklist
- [x] ~Unit~ Integration Tests
- [ ] Documentation
- [ ] Schema (porter.yaml)

If this is your first pull request, please add your name to the bottom of our [Contributors][contributors] list. Thank you for making Porter better! 🙇‍♀️

[contributors]: /CONTRIBUTORS.md

vdice: Still trying to figure out why the archive shasum is still non-deterministic in the integration test on CI... thoughts @carolynvs ? Repeated runs locally pass... 🤔 
carolynvs: Sorry been focused 100% on the infra migration from this week and haven't had time yet to consider this problem. I won't get a chance to look at this until Monday.

Patch:
diff --git a/cmd/porter/mixins.go b/cmd/porter/mixins.go
index 20fcb866..bde12308 100644
--- a/cmd/porter/mixins.go
+++ b/cmd/porter/mixins.go
@@ -1,6 +1,8 @@
 package main
 
 import (
+	"fmt"
+
 	"get.porter.sh/porter/pkg/mixin"
 	"get.porter.sh/porter/pkg/pkgmgmt"
 	"get.porter.sh/porter/pkg/pkgmgmt/feed"
@@ -55,9 +57,7 @@ func buildMixinsSearchCommand(p *porter.Porter) *cobra.Command {
 	cmd := &cobra.Command{
 		Use:   "search [QUERY]",
 		Short: "Search available mixins",
-		Long: `Search available mixins. You can specify an optional mixin name query, where the results are filtered by mixins whose name contains the query term.
-
-By default the community mixin index at https://cdn.porter.sh/mixins/index.json is searched. To search from a mirror, set the environment variable PORTER_MIRROR, or mirror in the Porter config file, with the value to replace https://cdn.porter.sh with.`,
+		Long:  "Search available mixins. You can specify an optional mixin name query, where the results are filtered by mixins whose name contains the query term.",
 		Example: `  porter mixin search
   porter mixin search helm
   porter mixin search -o json`,
@@ -69,11 +69,8 @@ By default the community mixin index at https://cdn.porter.sh/mixins/index.json
 		},
 	}
 
-	flags := cmd.Flags()
-	flags.StringVarP(&opts.RawFormat, "output", "o", "table",
+	cmd.Flags().StringVarP(&opts.RawFormat, "output", "o", "table",
 		"Output format, allowed values are: table, json, yaml")
-	flags.StringVar(&opts.Mirror, "mirror", pkgmgmt.DefaultPackageMirror,
-		"Mirror of official Porter assets")
 
 	return cmd
 }
@@ -83,9 +80,6 @@ func BuildMixinInstallCommand(p *porter.Porter) *cobra.Command {
 	cmd := &cobra.Command{
 		Use:   "install NAME",
 		Short: "Install a mixin",
-		Long: `Install a mixin.
-
-By default mixins are downloaded from the official Porter mixin feed at https://cdn.porter.sh/mixins/atom.xml. To download from a mirror, set the environment variable PORTER_MIRROR, or mirror in the Porter config file, with the value to replace https://cdn.porter.sh with.`,
 		Example: `  porter mixin install helm --url https://cdn.porter.sh/mixins/helm
   porter mixin install helm --feed-url https://cdn.porter.sh/mixins/atom.xml
   porter mixin install azure --version v0.4.0-ralpha.1+dubonnet --url https://cdn.porter.sh/mixins/azure
@@ -98,15 +92,12 @@ By default mixins are downloaded from the official Porter mixin feed at https://
 		},
 	}
 
-	flags := cmd.Flags()
-	flags.StringVarP(&opts.Version, "version", "v", "latest",
+	cmd.Flags().StringVarP(&opts.Version, "version", "v", "latest",
 		"The mixin version. This can either be a version number, or a tagged release like 'latest' or 'canary'")
-	flags.StringVar(&opts.URL, "url", "",
+	cmd.Flags().StringVar(&opts.URL, "url", "",
 		"URL from where the mixin can be downloaded, for example https://github.com/org/proj/releases/downloads")
-	flags.StringVar(&opts.FeedURL, "feed-url", "",
-		"URL of an atom feed where the mixin can be downloaded. Defaults to the official Porter mixin feed.")
-	flags.StringVar(&opts.Mirror, "mirror", pkgmgmt.DefaultPackageMirror,
-		"Mirror of official Porter assets")
+	cmd.Flags().StringVar(&opts.FeedURL, "feed-url", "",
+		fmt.Sprintf(`URL of an atom feed where the mixin can be downloaded (default %s)`, mixin.DefaultFeedUrl))
 	return cmd
 }
 
diff --git a/cmd/porter/plugins.go b/cmd/porter/plugins.go
index 2f23105e..b292769b 100644
--- a/cmd/porter/plugins.go
+++ b/cmd/porter/plugins.go
@@ -1,6 +1,8 @@
 package main
 
 import (
+	"fmt"
+
 	"get.porter.sh/porter/pkg/pkgmgmt"
 	"get.porter.sh/porter/pkg/plugins"
 	"get.porter.sh/porter/pkg/porter"
@@ -55,9 +57,7 @@ func buildPluginSearchCommand(p *porter.Porter) *cobra.Command {
 	cmd := &cobra.Command{
 		Use:   "search [QUERY]",
 		Short: "Search available plugins",
-		Long: `Search available plugins. You can specify an optional plugin name query, where the results are filtered by plugins whose name contains the query term.
-
-By default the community plugin index at https://cdn.porter.sh/plugins/index.json is searched. To search from a mirror, set the environment variable PORTER_MIRROR, or mirror in the Porter config file, with the value to replace https://cdn.porter.sh with.`,
+		Long:  "Search available plugins. You can specify an optional plugin name query, where the results are filtered by plugins whose name contains the query term.",
 		Example: `  porter plugin search
   porter plugin search azure
   porter plugin search -o json`,
@@ -69,11 +69,8 @@ By default the community plugin index at https://cdn.porter.sh/plugins/index.jso
 		},
 	}
 
-	flags := cmd.Flags()
-	flags.StringVarP(&opts.RawFormat, "output", "o", "table",
+	cmd.Flags().StringVarP(&opts.RawFormat, "output", "o", "table",
 		"Output format, allowed values are: table, json, yaml")
-	flags.StringVar(&opts.Mirror, "mirror", pkgmgmt.DefaultPackageMirror,
-		"Mirror of official Porter assets")
 
 	return cmd
 }
@@ -103,9 +100,6 @@ func BuildPluginInstallCommand(p *porter.Porter) *cobra.Command {
 	cmd := &cobra.Command{
 		Use:   "install NAME",
 		Short: "Install a plugin",
-		Long: `Install a plugin.
-
-By default plugins are downloaded from the official Porter plugin feed at https://cdn.porter.sh/plugins/atom.xml. To download from a mirror, set the environment variable PORTER_MIRROR, or mirror in the Porter config file, with the value to replace https://cdn.porter.sh with.`,
 		Example: `  porter plugin install azure  
   porter plugin install azure --url https://cdn.porter.sh/plugins/azure
   porter plugin install azure --feed-url https://cdn.porter.sh/plugins/atom.xml
@@ -119,16 +113,12 @@ By default plugins are downloaded from the official Porter plugin feed at https:
 		},
 	}
 
-	flags := cmd.Flags()
-	flags.StringVarP(&opts.Version, "version", "v", "latest",
+	cmd.Flags().StringVarP(&opts.Version, "version", "v", "latest",
 		"The plugin version. This can either be a version number, or a tagged release like 'latest' or 'canary'")
-	flags.StringVar(&opts.URL, "url", "",
+	cmd.Flags().StringVar(&opts.URL, "url", "",
 		"URL from where the plugin can be downloaded, for example https://github.com/org/proj/releases/downloads")
-	flags.StringVar(&opts.FeedURL, "feed-url", "",
-		"URL of an atom feed where the plugin can be downloaded. Defaults to the official Porter plugin feed.")
-	flags.StringVar(&opts.Mirror, "mirror", pkgmgmt.DefaultPackageMirror,
-		"Mirror of official Porter assets")
-
+	cmd.Flags().StringVar(&opts.FeedURL, "feed-url", "",
+		fmt.Sprintf(`URL of an atom feed where the plugin can be downloaded (default %s)`, plugins.DefaultFeedUrl))
 	return cmd
 }
 
diff --git a/docs/content/compatible-registries.md b/docs/content/compatible-registries.md
index 15d3fc51..f62ff1fd 100644
--- a/docs/content/compatible-registries.md
+++ b/docs/content/compatible-registries.md
@@ -18,7 +18,7 @@ out a particular registry and know that it will work for you.
 | Artifactory | No |
 | **Docker Hub** | **Yes** |
 | **DigitalOcean Container Registry** | **Yes** |
-| **Amazon Elastic Container Registry (ECR)** | **[Yes*](#notes)** |
+| Amazon Elastic Container Registry (ECR) | No |
 | **Google Artifact Registry** | **Yes** | 
 | Google Cloud Registry (GCR) | No | 
 | **GitHub Container Registry (GHCR)** | **Yes** | 
@@ -27,12 +27,7 @@ out a particular registry and know that it will work for you.
 | Nexus | No |
 | Quay | No |
 
-If you test a registry with Porter and find that this page is out of date, [please
-let us know](https://github.com/deislabs/porter/issues/new)!
+If you are a registry or user and know that this page is out of date, [please
+let us know!](https://github.com/deislabs/porter/issues/new)
 
-### Notes
-
-* Amazon Elastic Container Registry (ECR) requires that you create the repository for the installer and the bundle before publishing.
-
-[cnab-to-oci]: https://github.com/cnabio/cnab-to-oci
-[oci-spec]: https://github.com/opencontainers/distribution-spec/blob/master/spec.md
+[cnab-to-oci]: https://github.com/docker/cnab-to-oci
diff --git a/docs/content/install.md b/docs/content/install.md
index 50b75f68..09bb3ce6 100644
--- a/docs/content/install.md
+++ b/docs/content/install.md
@@ -13,11 +13,9 @@ We have a few release types available for you to use:
 * [Older Version](#older-version)
 
 You can also install and manage [mixins](#mixins) and [plugins](#plugins) using
-porter, and use the [Porter VS Code Extension][vscode-ext] to help author
+porter, and use the [Porter VS Code Extension][vscode-ext] for help authoring
 bundles.
 
-All the scripts for Porter v0.37.3+ support [customizing the installation through parameters](#install-script-parameters).
-
 [vscode-ext]: https://marketplace.visualstudio.com/items?itemName=ms-kubernetes-tools.porter-vscode
 [ps-link]: https://www.howtogeek.com/126469/how-to-create-a-powershell-profile/
 [mailing list]: https://groups.io/g/porter
@@ -131,64 +129,3 @@ All of the Porter-authored plugins are published to `https://cdn.porter.sh/plugi
 
 [releases]: https://github.com/getporter/porter/releases
 
-
-
-# Install Script Parameters
-
-The installation scripts provide the following parameters. Parameters can be specified with environment variables for the macOS and Linux scripts, and on Windows they are named parameters in the script.
-
-## PORTER_HOME
-
-Location where Porter is installed (defaults to ~/.porter).
-
-**Posix Shells**
-```bash
-PORTER_HOME=/alt/porter/home curl -L REPLACE_WITH_INSTALL_URL | bash
-```
-
-**PowerShell**
-```powershell
-iwr REPLACE_WITH_INSTALL_URL -OutFile install-porter.ps1 -UseBasicParsing
-.\install-porter.ps1 -PORTER_HOME C:\alt\porter\home
-```
-
-## PORTER_MIRROR
-
-Base URL where Porter assets, such as binaries and atom feeds, are downloaded.
-This lets you set up an internal mirror. Note that atom feeds and index files
-should be updated in the mirror to point to the mirrored location. Porter does
-not alter the contents of these files.
-
-**Posix Shells**
-```bash
-PORTER_MIRROR=https://example.com/porter curl -L REPLACE_WITH_INSTALL_URL | bash
-```
-
-**PowerShell**
-```powershell
-iwr REPLACE_WITH_INSTALL_URL -OutFile install-porter.ps1 -UseBasicParsing
-.\install-porter.ps1 -PORTER_MIRROR https://example.com/porter
-```
-
-### URL Structure
-
-Configuring a mirror of Porter's assets is out of scope of this document.
-Reach out on the Porter [mailing list] for assistance.
-
-Below is the general structure for Porter's asset URLs:
-
-```
-PERMALINK/
-  - install-linux.sh
-  - install-mac.sh
-  - install-windows.ps1
-  - porter-GOOS-GOARCH[FILE_EXT]
-mixins/
-  - atom.xml
-  - index.json
-  - MIXIN/PERMALINK/MIXIN-GOOS-GOARCH[FILE_EXT]
-plugins/
-  - atom.xml
-  - index.json
-  - PLUGIN/PERMALINK/PLUGIN-GOOS-GOARCH[FILE_EXT]
-```
diff --git a/examples/azure-terraform/porter.yaml b/examples/azure-terraform/porter.yaml
index 75f3ae27..715751c9 100644
--- a/examples/azure-terraform/porter.yaml
+++ b/examples/azure-terraform/porter.yaml
@@ -27,7 +27,7 @@ credentials:
   - name: client_secret
     env: AZURE_CLIENT_SECRET
 
-## This section defines what parameters are used by the bundle. These parameters are used by various 
+## This section defines what paramters are used by the bundle. These parameters are used by various 
 ## steps within the bundle
 parameters:
   - name: location
diff --git a/infra/cdn.md b/infra/cdn.md
index 20ebe0a9..aaf4d805 100644
--- a/infra/cdn.md
+++ b/infra/cdn.md
@@ -2,22 +2,20 @@
 
 Porter uses a variety of services to distribute its CLI, mixin and plugin binaries.
 
-## GitHub Releases
+## Azure Storage
 
-Binaries and install scripts are attached to GitHub releases upon relevant events in our GitHub repositories, such as merges to the "main" branch (producing `canary` artifacts) and official releases (producing `latest` and semver-tagged artifacts). We create releases for the following tags:
+Binaries are uploaded to [Azure Blob Storage](https://azure.microsoft.com/en-us/services/storage/blobs/) upon relevant events in our GitHub repositories, such as merges to the "main" branch (producing `canary` artifacts) and official releases (producing `latest` and semver-tagged artifacts).
 
-* vX.Y.Z - a tagged version of Porter. Recommended.
-* latest - the most recent tagged version of Porter. Stable but you should use a specific version in prod.
-* canary - the tip of the main branch. Unstable.
+Although it is possible to provide URLs directly to the stored resources, we'd be tightly coupled to a particular storage account and layout, not to mention ungainly URLs.  Therefore, we utilize the services below to achieve flexibility and control over asset links.
 
-We use our own tag for latest (instead of relying upon GitHub's latest release logic) so that we have consistent URLs.
+## DNS for porter.sh
 
-## GitHub Packages Repo
+DNS entries for the `porter.sh` domain are managed via a [Netlify](https://www.netlify.com/) account.  A `cdn` CNAME record exists in this configuration such that `cdn.porter.sh` can represent the hostname for all artifact URLs.
 
-We have a [packages](https://github.com/getporter/packages) repository that has our official mixin and plugin atom feeds used by porter mixin install and porter plugin install. It also contains an index of all known mixins and plugins from both the Porter Authors and the community, which is used by porter mixins search and porter plugins search.
+## Azure Front Door
 
-## DNS for porter.sh
+We use [Azure Front Door](https://azure.microsoft.com/en-us/services/frontdoor/) to route incoming requests involving the `https://cdn.porter.sh` hostname to their corresponding assets in storage.  This is where TLS certificate material for our custom domain is managed, along with routing rules corresponding to various classes of assets (mixins, plugins, etc.)
 
-DNS entries for the `porter.sh` domain are managed via a [Netlify](https://www.netlify.com/) account.  A `cdn` CNAME record exists in this configuration such that `cdn.porter.sh` can represent the hostname for all artifact URLs.
+## Azure Web Application Firewall
 
-Netlify handles redirecting traffic to cdn.porter.sh to the appropriate backing store (either a GH release artifact or a file in a repo).
+As a first stop for all incoming traffic, we use [Azure Web Application Firewall](https://docs.microsoft.com/en-us/azure/web-application-firewall/) to filter out requests involving invalid URIs.  This ensures that most of the traffic actually reaching Azure Front Door corresponds to actual assets.
\ No newline at end of file
diff --git a/pkg/build/build.go b/pkg/build/build.go
index e337be78..4f176a54 100644
--- a/pkg/build/build.go
+++ b/pkg/build/build.go
@@ -6,7 +6,7 @@ import (
 
 var (
 	// DOCKER_FILE is the file generated before running a docker build.
-	DOCKER_FILE = filepath.Join(LOCAL_CNAB, "Dockerfile")
+	DOCKER_FILE = "Dockerfile"
 
 	// LOCAL_CNAB is the generated directory where porter stages the /cnab directory.
 	LOCAL_CNAB = ".cnab"
diff --git a/pkg/build/dockerfile-generator.go b/pkg/build/dockerfile-generator.go
index 7118e739..555f30e8 100644
--- a/pkg/build/dockerfile-generator.go
+++ b/pkg/build/dockerfile-generator.go
@@ -198,6 +198,9 @@ func (g *DockerfileGenerator) buildMixinsSection() ([]string, error) {
 }
 
 func (g *DockerfileGenerator) PrepareFilesystem() error {
+	// clean up previously generated files
+	g.FileSystem.Remove(DOCKER_FILE)
+
 	fmt.Fprintf(g.Out, "Copying porter runtime ===> \n")
 
 	runTmpl, err := g.Templates.GetRunScript()
diff --git a/pkg/build/provider/docker.go b/pkg/build/provider/docker.go
index 1baa7929..d4f9aeba 100644
--- a/pkg/build/provider/docker.go
+++ b/pkg/build/provider/docker.go
@@ -4,7 +4,6 @@ import (
 	"context"
 	"fmt"
 	"io/ioutil"
-	"path/filepath"
 
 	"get.porter.sh/porter/pkg/build"
 	portercontext "get.porter.sh/porter/pkg/context"
@@ -36,7 +35,7 @@ func (b *DockerBuilder) BuildInvocationImage(manifest *manifest.Manifest) error
 		PullParent:     false,
 		Remove:         true,
 		Tags:           []string{manifest.Image},
-		Dockerfile:     filepath.ToSlash(build.DOCKER_FILE),
+		Dockerfile:     build.DOCKER_FILE,
 		BuildArgs: map[string]*string{
 			"BUNDLE_DIR": &build.BUNDLE_DIR,
 		},
diff --git a/pkg/manifest/manifest.go b/pkg/manifest/manifest.go
index ef41c8ca..20509dc7 100644
--- a/pkg/manifest/manifest.go
+++ b/pkg/manifest/manifest.go
@@ -770,7 +770,7 @@ func (s *Step) GetDescription() (string, error) {
 	children := s.Data[mixinName]
 	d, ok := children.(map[string]interface{})["description"]
 	if !ok {
-		return "", nil
+		return "", errors.Errorf("mixin step (%s) missing description", mixinName)
 	}
 	desc, ok := d.(string)
 	if !ok {
diff --git a/pkg/mixin/install.go b/pkg/mixin/install.go
index 34dae3a9..188eb464 100644
--- a/pkg/mixin/install.go
+++ b/pkg/mixin/install.go
@@ -4,11 +4,13 @@ import (
 	"get.porter.sh/porter/pkg/pkgmgmt"
 )
 
+const DefaultFeedUrl = "https://cdn.porter.sh/mixins/atom.xml"
+
 type InstallOptions struct {
 	pkgmgmt.InstallOptions
 }
 
 func (o *InstallOptions) Validate(args []string) error {
-	o.PackageType = "mixin"
+	o.DefaultFeedURL = DefaultFeedUrl
 	return o.InstallOptions.Validate(args)
 }
diff --git a/pkg/pkgmgmt/client/install.go b/pkg/pkgmgmt/client/install.go
index 0535018d..f147f53d 100644
--- a/pkg/pkgmgmt/client/install.go
+++ b/pkg/pkgmgmt/client/install.go
@@ -9,7 +9,9 @@ import (
 	"path"
 	"path/filepath"
 	"runtime"
+	"time"
 
+	"get.porter.sh/porter/pkg"
 	"get.porter.sh/porter/pkg/pkgmgmt"
 	"get.porter.sh/porter/pkg/pkgmgmt/feed"
 	"github.com/pkg/errors"
@@ -164,12 +166,20 @@ func (fs *FileSystem) downloadFile(url url.URL, destPath string, executable bool
 		return errors.Wrapf(err, "error creating web request to %s", url.String())
 	}
 
+	// Add debugging headers to our request
+	req.Header.Set("X-Azure-DebugInfo", "1")
+	userAgent := fmt.Sprintf("porter/%s porter_trace_%d %s", pkg.Version, time.Now().UnixNano(), req.UserAgent())
+	if fs.Debug {
+		fmt.Fprintln(fs.Err, "PORTER_TRACE:", userAgent)
+	}
+
+	req.Header.Set("User-Agent", userAgent)
 	resp, err := http.DefaultClient.Do(req)
 	if err != nil {
-		return errors.Wrapf(err, "error downloading %s", url.String())
+		return errors.Wrapf(err, "error downloading %s\nPlease include the following information in any bug reports:\nPORTER_TRACE: %s", url.String(), userAgent)
 	}
 	if resp.StatusCode != 200 {
-		return errors.Errorf("bad status returned when downloading %s (%d) %s", url.String(), resp.StatusCode, resp.Status)
+		return errors.Errorf("bad status returned when downloading %s (%d) %s\nPlease include the following information in any bug reports:\nPORTER_TRACE: %s\nHEADERS: %#v", url.String(), resp.StatusCode, resp.Status, userAgent, resp.Header)
 	}
 	defer resp.Body.Close()
 
diff --git a/pkg/pkgmgmt/download_options.go b/pkg/pkgmgmt/download_options.go
deleted file mode 100644
index 49f1a82b..00000000
--- a/pkg/pkgmgmt/download_options.go
+++ /dev/null
@@ -1,45 +0,0 @@
-package pkgmgmt
-
-import (
-	"net/url"
-
-	"github.com/pkg/errors"
-)
-
-// DefaultPackageMirror is the default location from which to download Porter assets, such as binaries, atom feeds and package indexes.
-const DefaultPackageMirror = "https://cdn.porter.sh"
-
-// GetDefaultPackageMirrorURL returns DefaultPackageMirror parsed as a url.URL
-func GetDefaultPackageMirrorURL() url.URL {
-	defaultMirror, _ := url.Parse(DefaultPackageMirror)
-	return *defaultMirror
-}
-
-// PackageDownloadOptions are options for downloading Porter packages, such as mixins and plugins.
-type PackageDownloadOptions struct {
-	Mirror       string
-	parsedMirror *url.URL
-}
-
-func (o *PackageDownloadOptions) Validate() error {
-	if o.Mirror == "" {
-		o.Mirror = DefaultPackageMirror
-	}
-
-	mirrorURL, err := url.Parse(o.Mirror)
-	if err != nil {
-		return errors.Wrapf(err, "Invalid --mirror %s", o.Mirror)
-	}
-	o.parsedMirror = mirrorURL
-
-	return nil
-}
-
-// GetMirror returns a copy of of the parsed Mirror that is safe to modify.
-func (o *PackageDownloadOptions) GetMirror() url.URL {
-	if o.parsedMirror == nil {
-		return GetDefaultPackageMirrorURL()
-	}
-
-	return *o.parsedMirror
-}
diff --git a/pkg/pkgmgmt/install.go b/pkg/pkgmgmt/install.go
index f5fd5d51..f71ff801 100644
--- a/pkg/pkgmgmt/install.go
+++ b/pkg/pkgmgmt/install.go
@@ -2,15 +2,12 @@ package pkgmgmt
 
 import (
 	"net/url"
-	"path"
 	"strings"
 
 	"github.com/pkg/errors"
 )
 
 type InstallOptions struct {
-	PackageDownloadOptions
-
 	Name          string
 	URL           string
 	FeedURL       string
@@ -18,47 +15,24 @@ type InstallOptions struct {
 	parsedURL     *url.URL
 	parsedFeedURL *url.URL
 
-	PackageType string
+	DefaultFeedURL string
 }
 
 // GetParsedURL returns a copy of of the parsed URL that is safe to modify.
 func (o *InstallOptions) GetParsedURL() url.URL {
-	if o.parsedURL == nil {
-		return url.URL{}
-	}
-
 	return *o.parsedURL
 }
 
 func (o *InstallOptions) GetParsedFeedURL() url.URL {
-	if o.parsedFeedURL == nil {
-		return o.defaultFeedURL()
-	}
-
 	return *o.parsedFeedURL
 }
 
-func (o *InstallOptions) defaultFeedURL() url.URL {
-	mirror := o.GetMirror()
-	mirror.Path = path.Join(mirror.Path, o.PackageType+"s", "atom.xml")
-	return mirror
-}
-
 func (o *InstallOptions) Validate(args []string) error {
-	if o.PackageType != "mixin" && o.PackageType != "plugin" {
-		return errors.Errorf("invalid package type %q. Please report this as a bug to Porter!", o.PackageType)
-	}
-
 	err := o.validateName(args)
 	if err != nil {
 		return err
 	}
 
-	err = o.PackageDownloadOptions.Validate()
-	if err != nil {
-		return err
-	}
-
 	err = o.validateFeedURL()
 	if err != nil {
 		return err
@@ -90,8 +64,7 @@ func (o *InstallOptions) validateURL() error {
 
 func (o *InstallOptions) validateFeedURL() error {
 	if o.URL == "" && o.FeedURL == "" {
-		feedURL := o.defaultFeedURL()
-		o.FeedURL = feedURL.String()
+		o.FeedURL = o.DefaultFeedURL
 	}
 
 	if o.FeedURL != "" {
diff --git a/pkg/pkgmgmt/pkgmgmt.go b/pkg/pkgmgmt/pkgmgmt.go
index 8bb3ec43..6ceed331 100644
--- a/pkg/pkgmgmt/pkgmgmt.go
+++ b/pkg/pkgmgmt/pkgmgmt.go
@@ -1,9 +1,8 @@
 package pkgmgmt
 
 import (
-	"net/url"
+	"fmt"
 	"os/exec"
-	"path"
 
 	"get.porter.sh/porter/pkg/context"
 )
@@ -42,8 +41,7 @@ type CommandOptions struct {
 	PreRun PreRunHandler
 }
 
-// GetPackageListURL returns the URL for package listings of the provided type.
-func GetPackageListURL(mirror url.URL, pkgType string) string {
-	mirror.Path = path.Join(mirror.Path, pkgType+"s", "index.json")
-	return mirror.String()
+// GetPackageListURL returns the URL for package listings of the provided type
+func GetPackageListURL(pkgType string) string {
+	return fmt.Sprintf("https://cdn.porter.sh/%ss/index.json", pkgType)
 }
diff --git a/pkg/plugins/install.go b/pkg/plugins/install.go
index b5a8554b..5b774903 100644
--- a/pkg/plugins/install.go
+++ b/pkg/plugins/install.go
@@ -4,11 +4,13 @@ import (
 	"get.porter.sh/porter/pkg/pkgmgmt"
 )
 
+const DefaultFeedUrl = "https://cdn.porter.sh/plugins/atom.xml"
+
 type InstallOptions struct {
 	pkgmgmt.InstallOptions
 }
 
 func (o *InstallOptions) Validate(args []string) error {
-	o.PackageType = "plugin"
+	o.DefaultFeedURL = DefaultFeedUrl
 	return o.InstallOptions.Validate(args)
 }
diff --git a/pkg/porter/archive.go b/pkg/porter/archive.go
index 92949778..e7f0fcb6 100644
--- a/pkg/porter/archive.go
+++ b/pkg/porter/archive.go
@@ -3,10 +3,11 @@ package porter
 import (
 	"fmt"
 	"io"
-	"io/ioutil"
 	"os"
 	"path/filepath"
+	"time"
 
+	"github.com/carolynvs/aferox"
 	"github.com/cnabio/cnab-go/bundle"
 	"github.com/cnabio/cnab-go/imagestore"
 	"github.com/cnabio/cnab-go/imagestore/construction"
@@ -72,6 +73,7 @@ func (p *Porter) Archive(opts ArchiveOptions) error {
 	dest, err := p.Config.FileSystem.OpenFile(opts.ArchiveFile, os.O_RDWR|os.O_CREATE, 0644)
 
 	exp := &exporter{
+		fs:                    p.Config.FileSystem,
 		out:                   p.Config.Out,
 		logs:                  p.Config.Out,
 		bundle:                bun,
@@ -86,6 +88,7 @@ func (p *Porter) Archive(opts ArchiveOptions) error {
 }
 
 type exporter struct {
+	fs                    aferox.Aferox
 	out                   io.Writer
 	logs                  io.Writer
 	bundle                bundle.Bundle
@@ -97,16 +100,16 @@ type exporter struct {
 func (ex *exporter) export() error {
 
 	name := ex.bundle.Name + "-" + ex.bundle.Version
-	archiveDir, err := ioutil.TempDir("", name)
+	archiveDir, err := ex.fs.TempDir("", name)
 	if err != nil {
 		return err
 	}
-	if err := os.MkdirAll(archiveDir, 0644); err != nil {
+	if err := ex.fs.MkdirAll(archiveDir, 0644); err != nil {
 		return err
 	}
-	defer os.RemoveAll(archiveDir)
+	defer ex.fs.RemoveAll(archiveDir)
 
-	to, err := os.OpenFile(filepath.Join(archiveDir, "bundle.json"), os.O_RDWR|os.O_CREATE, 0666)
+	to, err := ex.fs.OpenFile(filepath.Join(archiveDir, "bundle.json"), os.O_RDWR|os.O_CREATE, 0666)
 	if err != nil {
 		return err
 	}
@@ -118,11 +121,15 @@ func (ex *exporter) export() error {
 
 	ex.imageStore, err = ex.imageStoreConstructor(imagestore.WithArchiveDir(archiveDir), imagestore.WithLogs(ex.logs))
 	if err != nil {
-		return fmt.Errorf("Error creating artifacts: %s", err)
+		return fmt.Errorf("error creating artifacts: %s", err)
 	}
 
 	if err := ex.prepareArtifacts(ex.bundle); err != nil {
-		return fmt.Errorf("Error preparing artifacts: %s", err)
+		return fmt.Errorf("error preparing artifacts: %s", err)
+	}
+
+	if err := ex.chtimes(archiveDir); err != nil {
+		return fmt.Errorf("error preparing artifacts: %s", err)
 	}
 
 	tarOptions := &archive.TarOptions{
@@ -140,6 +147,28 @@ func (ex *exporter) export() error {
 	return err
 }
 
+// chtimes updates all paths under the provided archive path with a constant
+// atime and mtime (Unix time 0), such that the shasum of the resulting archive
+// will not change between repeated archival executions using the same bundle.
+// See: https://unix.stackexchange.com/questions/346789/compressing-two-identical-folders-give-different-result
+func (ex *exporter) chtimes(path string) error {
+	err := filepath.Walk(path,
+		func(subpath string, info os.FileInfo, err error) error {
+			if err != nil {
+				return err
+			}
+			err = ex.fs.Chtimes(subpath, time.Unix(0, 0), time.Unix(0, 0))
+			if err != nil {
+				return err
+			}
+			return nil
+		})
+	if err != nil {
+		return err
+	}
+	return nil
+}
+
 // prepareArtifacts pulls all images, verifies their digests and
 // saves them to a directory called artifacts/ in the bundle directory
 func (ex *exporter) prepareArtifacts(bun bundle.Bundle) error {
diff --git a/pkg/porter/build.go b/pkg/porter/build.go
index 52c6cf8c..bba40c91 100644
--- a/pkg/porter/build.go
+++ b/pkg/porter/build.go
@@ -45,12 +45,6 @@ func (o *BuildOptions) Validate(cxt *context.Context) error {
 func (p *Porter) Build(opts BuildOptions) error {
 	opts.Apply(p.Context)
 
-	// Start with a fresh .cnab directory before building
-	err := p.FileSystem.RemoveAll(build.LOCAL_CNAB)
-	if err != nil {
-		return errors.Wrap(err, "could not cleanup generated .cnab directory before building")
-	}
-
 	// Generate Porter's canonical version of the user-provided manifest
 	if err := p.generateInternalManifest(opts); err != nil {
 		return errors.Wrap(err, "unable to generate manifest")
diff --git a/pkg/porter/explain.go b/pkg/porter/explain.go
index f3b63264..21e56ab0 100644
--- a/pkg/porter/explain.go
+++ b/pkg/porter/explain.go
@@ -38,7 +38,6 @@ type PrintableCredential struct {
 	Name        string `json:"name" yaml:"name"`
 	Description string `json:"description" yaml:"description"`
 	Required    bool   `json:"required" yaml:"required"`
-	ApplyTo     string `json:"applyTo" yaml:"applyTo"`
 }
 
 type SortPrintableCredential []PrintableCredential
@@ -205,7 +204,7 @@ func generatePrintable(bun bundle.Bundle, action string) (*PrintableBundle, erro
 		PorterVersion: stamp.Version,
 	}
 
-	actions := make([]PrintableAction, 0, len(bun.Actions))
+	actions := []PrintableAction{}
 	for a, v := range bun.Actions {
 		pa := PrintableAction{}
 		pa.Name = a
@@ -216,21 +215,18 @@ func generatePrintable(bun bundle.Bundle, action string) (*PrintableBundle, erro
 	}
 	sort.Sort(SortPrintableAction(actions))
 
-	creds := make([]PrintableCredential, 0, len(bun.Credentials))
+	creds := []PrintableCredential{}
 	for c, v := range bun.Credentials {
 		pc := PrintableCredential{}
 		pc.Name = c
 		pc.Description = v.Description
 		pc.Required = v.Required
-		pc.ApplyTo = generateApplyToString(v.ApplyTo)
 
-		if action == "" || v.AppliesTo(action) {
-			creds = append(creds, pc)
-		}
+		creds = append(creds, pc)
 	}
 	sort.Sort(SortPrintableCredential(creds))
 
-	params := make([]PrintableParameter, 0, len(bun.Parameters))
+	params := []PrintableParameter{}
 	for p, v := range bun.Parameters {
 		if parameters.IsInternal(p, bun) {
 			continue
@@ -250,13 +246,13 @@ func generatePrintable(bun bundle.Bundle, action string) (*PrintableBundle, erro
 		pp.Required = v.Required
 		pp.Description = v.Description
 
-		if action == "" || v.AppliesTo(action) {
+		if v.AppliesTo(action) {
 			params = append(params, pp)
 		}
 	}
 	sort.Sort(SortPrintableParameter(params))
 
-	outputs := make([]PrintableOutput, 0, len(bun.Outputs))
+	outputs := []PrintableOutput{}
 	for o, v := range bun.Outputs {
 		def, ok := bun.Definitions[v.Definition]
 		if !ok {
@@ -277,13 +273,13 @@ func generatePrintable(bun bundle.Bundle, action string) (*PrintableBundle, erro
 	}
 	sort.Sort(SortPrintableOutput(outputs))
 
+	dependencies := []PrintableDependency{}
 	solver := &extensions.DependencySolver{}
 	deps, err := solver.ResolveDependencies(bun)
 	if err != nil {
 		return nil, errors.Wrapf(err, "error executing dependencies")
 	}
 
-	dependencies := make([]PrintableDependency, 0, len(deps))
 	for _, dep := range deps {
 		pd := PrintableDependency{}
 		pd.Alias = dep.Alias
@@ -345,9 +341,9 @@ func (p *Porter) printCredentialsExplainTable(bun *PrintableBundle) error {
 			if !ok {
 				return nil
 			}
-			return []interface{}{c.Name, c.Description, c.Required, c.ApplyTo}
+			return []interface{}{c.Name, c.Description, c.Required}
 		}
-	return printer.PrintTable(p.Out, bun.Credentials, printCredRow, "Name", "Description", "Required", "Applies To")
+	return printer.PrintTable(p.Out, bun.Credentials, printCredRow, "Name", "Description", "Required")
 }
 
 func (p *Porter) printParametersExplainBlock(bun *PrintableBundle) error {
diff --git a/pkg/porter/packages.go b/pkg/porter/packages.go
index 5c02e52e..af3589b1 100644
--- a/pkg/porter/packages.go
+++ b/pkg/porter/packages.go
@@ -15,7 +15,6 @@ type SearchOptions struct {
 	Name string
 	Type string
 	printer.PrintOptions
-	pkgmgmt.PackageDownloadOptions
 }
 
 // Validate validates the arguments provided to a search command
@@ -29,11 +28,6 @@ func (o *SearchOptions) Validate(args []string) error {
 		return err
 	}
 
-	err = o.PackageDownloadOptions.Validate()
-	if err != nil {
-		return err
-	}
-
 	return o.ParseFormat()
 }
 
@@ -52,7 +46,7 @@ func (o *SearchOptions) validatePackageName(args []string) error {
 
 // SearchPackages searches the provided package list according to the provided options
 func (p *Porter) SearchPackages(opts SearchOptions) error {
-	url := pkgmgmt.GetPackageListURL(opts.GetMirror(), opts.Type)
+	url := pkgmgmt.GetPackageListURL(opts.Type)
 	list, err := pkgmgmt.GetPackageListings(url)
 	if err != nil {
 		return err
diff --git a/pkg/runtime/runtime.go b/pkg/runtime/runtime.go
index 276192fc..b7d01f2c 100644
--- a/pkg/runtime/runtime.go
+++ b/pkg/runtime/runtime.go
@@ -74,9 +74,7 @@ func (r *PorterRuntime) Execute(rm *RuntimeManifest) error {
 			}
 
 			description, _ := step.GetDescription()
-			if len(description) > 0 {
-				fmt.Fprintln(r.Out, description)
-			}
+			fmt.Fprintln(r.Out, description)
 
 			// Hand over values needing masking in context output streams
 			r.Context.SetSensitiveValues(r.RuntimeManifest.GetSensitiveValues())
diff --git a/scripts/install/install-linux.sh b/scripts/install/install-linux.sh
index f0f1f9ad..cdd55c9c 100755
--- a/scripts/install/install-linux.sh
+++ b/scripts/install/install-linux.sh
@@ -1,23 +1,17 @@
 #!/usr/bin/env bash
 set -xeuo pipefail
 
-# Installs the porter CLI for a single user.
-# PORTER_HOME:      Location where Porter is installed (defaults to ~/.porter).
-# PORTER_MIRROR:       Base URL where Porter assets, such as binaries and atom feeds, are downloaded. This lets you
-#                   setup an internal mirror.
-# PORTER_PERMALINK: The version of Porter to install, such as vX.Y.Z, latest or canary.
-# PKG_PERMALINK:    The version of mixins and plugins to install, such as latest or canary.
-
-export PORTER_HOME=${PORTER_HOME:-~/.porter}
-export PORTER_MIRROR=${PORTER_MIRROR:-https://cdn.porter.sh}
+PORTER_HOME=~/.porter
+PORTER_URL=https://cdn.porter.sh
 PORTER_PERMALINK=${PORTER_PERMALINK:-latest}
 PKG_PERMALINK=${PKG_PERMALINK:-latest}
-
-echo "Installing porter@$PORTER_PERMALINK to $PORTER_HOME from $PORTER_MIRROR"
+PORTER_TRACE=$(date +%s_%N)
+echo "Installing porter to $PORTER_HOME"
+echo "PORTER_TRACE: $PORTER_TRACE"
 
 mkdir -p $PORTER_HOME/runtimes
 
-curl -fsSLo $PORTER_HOME/porter $PORTER_MIRROR/$PORTER_PERMALINK/porter-linux-amd64
+curl --http1.1 -v -H "X-Azure-DebugInfo: 1" -A "curl porter_install/$PORTER_PERMALINK porter_trace_$PORTER_TRACE"  -fsSLo $PORTER_HOME/porter $PORTER_URL/$PORTER_PERMALINK/porter-linux-amd64
 chmod +x $PORTER_HOME/porter
 cp $PORTER_HOME/porter $PORTER_HOME/runtimes/porter-runtime
 echo Installed `$PORTER_HOME/porter version`
diff --git a/scripts/install/install-mac.sh b/scripts/install/install-mac.sh
index 35bc9b3f..2a8033f4 100755
--- a/scripts/install/install-mac.sh
+++ b/scripts/install/install-mac.sh
@@ -1,24 +1,18 @@
 #!/usr/bin/env bash
 set -xeuo pipefail
 
-# Installs the porter CLI for a single user.
-# PORTER_HOME:      Location where Porter is installed (defaults to ~/.porter).
-# PORTER_MIRROR:       Base URL where Porter assets, such as binaries and atom feeds, are downloaded. This lets you
-#                   setup an internal mirror.
-# PORTER_PERMALINK: The version of Porter to install, such as vX.Y.Z, latest or canary.
-# PKG_PERMALINK:    The version of mixins and plugins to install, such as latest or canary.
-
-export PORTER_HOME=${PORTER_HOME:-~/.porter}
-export PORTER_MIRROR=${PORTER_MIRROR:-https://cdn.porter.sh}
+PORTER_HOME=~/.porter
+PORTER_URL=https://cdn.porter.sh
 PORTER_PERMALINK=${PORTER_PERMALINK:-latest}
 PKG_PERMALINK=${PKG_PERMALINK:-latest}
-
-echo "Installing porter@$PORTER_PERMALINK to $PORTER_HOME from $PORTER_MIRROR"
+PORTER_TRACE=$(date +%s_%N)
+echo "Installing porter to $PORTER_HOME"
+echo "PORTER_TRACE: $PORTER_TRACE"
 
 mkdir -p $PORTER_HOME/runtimes
 
-curl -fsSLo $PORTER_HOME/porter $PORTER_MIRROR/$PORTER_PERMALINK/porter-darwin-amd64
-curl -fsSLo $PORTER_HOME/runtimes/porter-runtime $PORTER_MIRROR/$PORTER_PERMALINK/porter-linux-amd64
+curl --http1.1 -v -H "X-Azure-DebugInfo: 1" -A "curl porter_install/$PORTER_PERMALINK porter_trace_$PORTER_TRACE" -fsSLo $PORTER_HOME/porter $PORTER_URL/$PORTER_PERMALINK/porter-darwin-amd64
+curl --http1.1 -v -H "X-Azure-DebugInfo: 1" -A "curl porter_install/$PORTER_PERMALINK porter_trace_$PORTER_TRACE" -fsSLo $PORTER_HOME/runtimes/porter-runtime $PORTER_URL/$PORTER_PERMALINK/porter-linux-amd64
 chmod +x $PORTER_HOME/porter
 chmod +x $PORTER_HOME/runtimes/porter-runtime
 echo Installed `$PORTER_HOME/porter version`
diff --git a/scripts/install/install-windows.ps1 b/scripts/install/install-windows.ps1
index d4f49400..c11cea4a 100755
--- a/scripts/install/install-windows.ps1
+++ b/scripts/install/install-windows.ps1
@@ -1,26 +1,14 @@
-# Installs the porter CLI for a single user.
-param(
-    [String]
-    # The version of Porter to install, such as vX.Y.Z, latest or canary.
-    $PORTER_PERMALINK='latest',
-    [String]
-    # The version of mixins and plugins to install, such as latest or canary.
-    $PKG_PERMALINK='latest',
-    [String]
-    # Location where Porter is installed (defaults to ~/.porter).
-    $PORTER_HOME="$env:USERPROFILE\.porter",
-    [String]
-    # Base URL where Porter assets, such as binaries and atom feeds, are downloaded. This lets you setup an internal mirror.
-    $PORTER_MIRROR="https://cdn.porter.sh")
+param([String]$PORTER_PERMALINK='latest', [String]$PKG_PERMALINK='latest')
 
-echo "Installing porter@$PORTER_PERMALINK to $PORTER_HOME from $PORTER_MIRROR"
+$PORTER_HOME="$env:USERPROFILE\.porter"
+$PORTER_URL="https://cdn.porter.sh"
+
+echo "Installing porter to $PORTER_HOME"
 
-$env:PORTER_HOME=$PORTER_HOME
-$env:PORTER_MIRROR=$PORTER_MIRROR
 mkdir -f $PORTER_HOME/runtimes
 
-(new-object System.Net.WebClient).DownloadFile("$PORTER_MIRROR/$PORTER_PERMALINK/porter-windows-amd64.exe", "$PORTER_HOME\porter.exe")
-(new-object System.Net.WebClient).DownloadFile("$PORTER_MIRROR/$PORTER_PERMALINK/porter-linux-amd64", "$PORTER_HOME\runtimes\porter-runtime")
+(new-object System.Net.WebClient).DownloadFile("$PORTER_URL/$PORTER_PERMALINK/porter-windows-amd64.exe", "$PORTER_HOME\porter.exe")
+(new-object System.Net.WebClient).DownloadFile("$PORTER_URL/$PORTER_PERMALINK/porter-linux-amd64", "$PORTER_HOME\runtimes\porter-runtime")
 echo "Installed $(& $PORTER_HOME\porter.exe version)"
 
 & $PORTER_HOME/porter mixin install exec --version $PKG_PERMALINK
diff --git a/workshop/porter-tf-aci/azure/bundle/porter.yaml b/workshop/porter-tf-aci/azure/bundle/porter.yaml
index 149ddae4..8802a24e 100644
--- a/workshop/porter-tf-aci/azure/bundle/porter.yaml
+++ b/workshop/porter-tf-aci/azure/bundle/porter.yaml
@@ -23,7 +23,7 @@ credentials:
   - name: client_secret
     env: AZURE_CLIENT_SECRET
 
-## This section defines what parameters are used by the bundle. These parameters are used by various 
+## This section defines what paramters are used by the bundle. These parameters are used by various 
 ## steps within the bundle
 parameters:
   - name: location
diff --git a/workshop/porter-tf/azure/porter.yaml b/workshop/porter-tf/azure/porter.yaml
index 08975abb..affd51e3 100644
--- a/workshop/porter-tf/azure/porter.yaml
+++ b/workshop/porter-tf/azure/porter.yaml
@@ -23,7 +23,7 @@ credentials:
   - name: client_secret
     env: AZURE_CLIENT_SECRET
 
-## This section defines what parameters are used by the bundle. These parameters are used by various 
+## This section defines what paramters are used by the bundle. These parameters are used by various 
 ## steps within the bundle
 parameters:
   - name: location


Test patch:
diff --git a/pkg/build/dockerfile-generator_test.go b/pkg/build/dockerfile-generator_test.go
index db17aee8..44939f20 100644
--- a/pkg/build/dockerfile-generator_test.go
+++ b/pkg/build/dockerfile-generator_test.go
@@ -278,19 +278,14 @@ func TestPorter_generateDockerfile(t *testing.T) {
 	err = g.GenerateDockerFile()
 	require.NoError(t, err)
 
-	wantDockerfilePath := ".cnab/Dockerfile"
-	dockerfileExists, err := c.FileSystem.Exists(wantDockerfilePath)
+	dockerfileExists, err := c.FileSystem.Exists("Dockerfile")
 	require.NoError(t, err)
 	require.True(t, dockerfileExists, "Dockerfile wasn't written")
 
-	f, _ := c.FileSystem.Stat(wantDockerfilePath)
+	f, _ := c.FileSystem.Stat("Dockerfile")
 	if f.Size() == 0 {
 		t.Fatalf("Dockerfile is empty")
 	}
-
-	// Verify that we didn't generate a Dockerfile at the root of the bundle dir
-	oldDockerfilePathExists, _ := c.FileSystem.Exists("Dockerfile")
-	assert.False(t, oldDockerfilePathExists, "expected the Dockerfile to be placed only at .cnab/Dockerfile, not at the root of the bundle directory")
 }
 
 func TestPorter_prepareDockerFilesystem(t *testing.T) {
diff --git a/pkg/manifest/testdata/porter.yaml b/pkg/manifest/testdata/porter.yaml
index 15d23c94..45b88fd2 100644
--- a/pkg/manifest/testdata/porter.yaml
+++ b/pkg/manifest/testdata/porter.yaml
@@ -13,6 +13,7 @@ dependencies:
 
 install:
   - exec:
+      description: "Install Hello World"
       command: bash
       flags:
         c: echo Hello World
diff --git a/pkg/mixin/install_test.go b/pkg/mixin/install_test.go
index f59ccb90..64296ecd 100644
--- a/pkg/mixin/install_test.go
+++ b/pkg/mixin/install_test.go
@@ -12,5 +12,5 @@ func TestInstallOptions_Validate(t *testing.T) {
 	opts := InstallOptions{}
 	err := opts.Validate([]string{"pkg1"})
 	require.NoError(t, err, "Validate failed")
-	assert.NotEmpty(t, opts.FeedURL, "Feed URL was not defaulted")
+	assert.Equal(t, DefaultFeedUrl, opts.FeedURL, "Feed URL was not defaulted to the mixin feed URL")
 }
diff --git a/pkg/pkgmgmt/client/install_test.go b/pkg/pkgmgmt/client/install_test.go
index ca490483..7f717f68 100644
--- a/pkg/pkgmgmt/client/install_test.go
+++ b/pkg/pkgmgmt/client/install_test.go
@@ -19,7 +19,11 @@ import (
 func TestFileSystem_InstallFromUrl(t *testing.T) {
 	// serve out a fake package
 	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-		fmt.Fprintf(w, "#!/usr/bin/env bash\necho i am a random package\n")
+		if r.Header.Get("X-Azure-DebugInfo") == "" ||
+			!strings.Contains(r.Header.Get("User-Agent"), "porter_trace") {
+			w.WriteHeader(400)
+		}
+		fmt.Fprintf(w, "#!/usr/bin/env bash\necho i am a mixxin\n")
 	}))
 	defer ts.Close()
 
@@ -27,19 +31,17 @@ func TestFileSystem_InstallFromUrl(t *testing.T) {
 	p := NewFileSystem(c.Config, "packages")
 
 	opts := pkgmgmt.InstallOptions{
-		PackageType: "mixin",
-		Version:     "latest",
-		URL:         ts.URL,
+		Version: "latest",
+		URL:     ts.URL,
 	}
-	err := opts.Validate([]string{"mypkg"})
-	require.NoError(t, err, "Validate failed")
+	opts.Validate([]string{"mixxin"})
 
-	err = p.Install(opts)
+	err := p.Install(opts)
 	require.NoError(t, err)
 
-	clientExists, _ := p.FileSystem.Exists("/root/.porter/packages/mypkg/mypkg")
+	clientExists, _ := p.FileSystem.Exists("/root/.porter/packages/mixxin/mixxin")
 	assert.True(t, clientExists)
-	runtimeExists, _ := p.FileSystem.Exists("/root/.porter/packages/mypkg/runtimes/mypkg-runtime")
+	runtimeExists, _ := p.FileSystem.Exists("/root/.porter/packages/mixxin/runtimes/mixxin-runtime")
 	assert.True(t, runtimeExists)
 }
 
@@ -66,12 +68,10 @@ func TestFileSystem_InstallFromFeedUrl(t *testing.T) {
 	p := NewFileSystem(c.Config, "packages")
 
 	opts := pkgmgmt.InstallOptions{
-		PackageType: "plugin",
-		Version:     "v1.2.4",
-		FeedURL:     ts.URL + "/atom.xml",
+		Version: "v1.2.4",
+		FeedURL: ts.URL + "/atom.xml",
 	}
-	err = opts.Validate([]string{"helm"})
-	require.NoError(t, err, "Validate failed")
+	opts.Validate([]string{"helm"})
 
 	err = p.Install(opts)
 	require.NoError(t, err)
@@ -88,7 +88,7 @@ func TestFileSystem_Install_RollbackMissingRuntime(t *testing.T) {
 		if strings.Contains(r.RequestURI, "linux-amd64") {
 			w.WriteHeader(400)
 		} else {
-			fmt.Fprintf(w, "#!/usr/bin/env bash\necho i am a client mypkg\n")
+			fmt.Fprintf(w, "#!/usr/bin/env bash\necho i am a client mixxin\n")
 		}
 	}))
 	defer ts.Close()
@@ -97,19 +97,18 @@ func TestFileSystem_Install_RollbackMissingRuntime(t *testing.T) {
 	p := NewFileSystem(c.Config, "packages")
 
 	parentDir, _ := p.GetPackagesDir()
-	pkgDir := path.Join(parentDir, "mypkg")
+	pkgDir := path.Join(parentDir, "mixxin")
 
 	opts := pkgmgmt.InstallOptions{
-		PackageType: "mixin",
-		Version:     "latest",
-		URL:         ts.URL,
+		Version: "latest",
+		URL:     ts.URL,
 	}
-	err := opts.Validate([]string{"mypkg"})
-	require.NoError(t, err, "Validate failed")
+	opts.Validate([]string{"mixxin"})
 
-	err = p.Install(opts)
+	err := p.Install(opts)
 	require.Error(t, err)
 	assert.Contains(t, err.Error(), "bad status returned when downloading")
+	assert.Contains(t, err.Error(), "porter_trace_", "The error message should contain our special debug user agent")
 
 	// Make sure the package directory was removed
 	dirExists, _ := p.FileSystem.DirExists(pkgDir)
@@ -122,19 +121,17 @@ func TestFileSystem_Install_PackageInfoSavedWhenNoFileExists(t *testing.T) {
 
 	packageURL := "https://cdn.porter.sh/mixins/helm"
 	opts := pkgmgmt.InstallOptions{
-		PackageType: "plugin",
-		Version:     "v1.2.4",
-		URL:         packageURL,
+		Version: "v1.2.4",
+		URL:     packageURL,
 	}
 	name := "helm"
-	err := opts.Validate([]string{name})
-	require.NoError(t, err, "Validate failed")
+	opts.Validate([]string{name})
 
 	// ensure cache.json does not exist (yet)
 	cacheExists, _ := p.FileSystem.Exists("/root/.porter/packages/cache.json")
 	assert.False(t, cacheExists)
 
-	err = p.savePackageInfo(opts)
+	err := p.savePackageInfo(opts)
 	require.NoError(t, err)
 
 	// cache.json should have been created
diff --git a/pkg/pkgmgmt/download_options_test.go b/pkg/pkgmgmt/download_options_test.go
deleted file mode 100644
index 9d586302..00000000
--- a/pkg/pkgmgmt/download_options_test.go
+++ /dev/null
@@ -1,45 +0,0 @@
-package pkgmgmt
-
-import (
-	"testing"
-
-	"github.com/stretchr/testify/assert"
-	"github.com/stretchr/testify/require"
-)
-
-func TestPackageDownloadOptions_Validate(t *testing.T) {
-	t.Run("unset", func(t *testing.T) {
-		opts := PackageDownloadOptions{}
-		require.NoError(t, opts.Validate())
-		assert.Equal(t, DefaultPackageMirror, opts.Mirror)
-	})
-
-	t.Run("valid url", func(t *testing.T) {
-		exampleMirror := "https://example.com"
-		opts := PackageDownloadOptions{Mirror: exampleMirror}
-		require.NoError(t, opts.Validate())
-		assert.Equal(t, exampleMirror, opts.Mirror)
-	})
-
-	t.Run("invalid url", func(t *testing.T) {
-		opts := PackageDownloadOptions{Mirror: "$://example.com"}
-		require.Error(t, opts.Validate())
-	})
-}
-
-func TestPackageDownloadOptions_GetMirror(t *testing.T) {
-	t.Run("mirror unset", func(t *testing.T) {
-		opts := PackageDownloadOptions{}
-		mirror := opts.GetMirror()
-		assert.Equal(t, DefaultPackageMirror, mirror.String())
-	})
-
-	t.Run("mirror set", func(t *testing.T) {
-		exampleMirror := "https://example.com"
-		opts := PackageDownloadOptions{Mirror: exampleMirror}
-		require.NoError(t, opts.Validate())
-
-		result := opts.GetMirror()
-		assert.Equal(t, exampleMirror, result.String())
-	})
-}
diff --git a/pkg/pkgmgmt/install_test.go b/pkg/pkgmgmt/install_test.go
index 3e5393bb..815c9c0b 100644
--- a/pkg/pkgmgmt/install_test.go
+++ b/pkg/pkgmgmt/install_test.go
@@ -41,37 +41,23 @@ func TestInstallOptions_DefaultVersion(t *testing.T) {
 }
 
 func TestInstallOptions_ValidateFeedURL(t *testing.T) {
-	t.Run("feed url unset, mirror unset", func(t *testing.T) {
+	t.Run("default feed url", func(t *testing.T) {
+		defaultFeedURL := "https://example.com/atom.xml"
 		opts := InstallOptions{
-			PackageType: "mixin",
+			DefaultFeedURL: defaultFeedURL,
 		}
-		err := opts.Validate([]string{"mypkg"})
+		err := opts.validateFeedURL()
 		require.NoError(t, err)
-		wantFeedURL := "https://cdn.porter.sh/mixins/atom.xml"
-		assert.Equal(t, wantFeedURL, opts.FeedURL, "fallback to the default feed url when nothing is specified")
+		assert.Equal(t, defaultFeedURL, opts.FeedURL, "fallback to the default feed url when nothing is specified")
 
 		parsedFeedURL := opts.GetParsedFeedURL()
-		assert.Equal(t, wantFeedURL, parsedFeedURL.String(), "validateFeedURL should parse the feed url")
-	})
-	t.Run("feed url unset, mirror set", func(t *testing.T) {
-		opts := InstallOptions{
-			PackageType:            "plugin",
-			PackageDownloadOptions: PackageDownloadOptions{Mirror: "https://example.com:81/porter"},
-		}
-		err := opts.Validate([]string{"mypkg"})
-		require.NoError(t, err)
-		wantFeedURL := "https://example.com:81/porter/plugins/atom.xml"
-		assert.Equal(t, wantFeedURL, opts.FeedURL, "fallback to the mirror when nothing is specified")
-
-		parsedFeedURL := opts.GetParsedFeedURL()
-		assert.Equal(t, wantFeedURL, parsedFeedURL.String(), "validateFeedURL should parse the feed url")
+		assert.Equal(t, defaultFeedURL, parsedFeedURL.String(), "validateFeedURL should parse the feed url")
 	})
 	t.Run("user specified feed url", func(t *testing.T) {
 		opts := InstallOptions{
-			PackageType: "mixin",
-			FeedURL:     "https://example.com/atom.xml",
+			FeedURL: "https://example.com/atom.xml",
 		}
-		err := opts.Validate([]string{"mypkg"})
+		err := opts.validateFeedURL()
 		require.NoError(t, err)
 
 		parsedFeedURL := opts.GetParsedFeedURL()
@@ -79,32 +65,24 @@ func TestInstallOptions_ValidateFeedURL(t *testing.T) {
 	})
 	t.Run("user specified url", func(t *testing.T) {
 		opts := InstallOptions{
-			PackageType: "plugin",
-			URL:         "https://example.com/mymixin",
+			URL: "https://example.com/mymixin",
 		}
-		err := opts.Validate([]string{"mypkg"})
+		err := opts.validateFeedURL()
 		require.NoError(t, err)
 		assert.Nil(t, opts.parsedFeedURL, "validateFeedURL shouldn't try to parse an empty URL")
 	})
 	t.Run("invalid feed url specified", func(t *testing.T) {
 		opts := InstallOptions{
-			PackageType: "mixin",
-			FeedURL:     "$://example.com",
+			FeedURL: "$://example.com",
 		}
-		err := opts.Validate([]string{"mypkg"})
+		err := opts.validateFeedURL()
 		assert.Contains(t, err.Error(), fmt.Sprintf("invalid --feed-url %s", opts.FeedURL))
 		assert.Contains(t, err.Error(), "first path segment in URL cannot contain colon")
 	})
 }
 
 func TestInstallOptions_ValidateURL(t *testing.T) {
-	t.Run("url unset, mirror unset", func(t *testing.T) {
-		opts := InstallOptions{}
-		err := opts.validateURL()
-		require.NoError(t, err)
-		assert.Nil(t, opts.parsedURL, "validateURL shouldn't try to parse an empty URL")
-	})
-	t.Run("url unset, mirror set", func(t *testing.T) {
+	t.Run("url not specified", func(t *testing.T) {
 		opts := InstallOptions{}
 		err := opts.validateURL()
 		require.NoError(t, err)
@@ -130,32 +108,11 @@ func TestInstallOptions_ValidateURL(t *testing.T) {
 }
 
 func TestInstallOptions_Validate(t *testing.T) {
-	t.Run("mixin", func(t *testing.T) {
-		opts := InstallOptions{
-			PackageType: "mixin",
-		}
-		err := opts.Validate([]string{"pkg"})
-		require.NoError(t, err, "Validate failed")
-		assert.NotEmpty(t, opts.FeedURL, "Validate should have defaulted the feed")
-		assert.NotEmpty(t, opts.Version, "Validate should have defaulted the version")
-		assert.NotEmpty(t, opts.Mirror, "Validate should have defaulted the mirror")
-	})
-	t.Run("plugin", func(t *testing.T) {
-		opts := InstallOptions{
-			PackageType: "mixin",
-		}
-		err := opts.Validate([]string{"pkg"})
-		require.NoError(t, err, "Validate failed")
-		assert.NotEmpty(t, opts.FeedURL, "Validate should have defaulted the feed")
-		assert.NotEmpty(t, opts.Version, "Validate should have defaulted the version")
-		assert.NotEmpty(t, opts.Mirror, "Validate should have defaulted the mirror")
-	})
-	t.Run("invalid package type", func(t *testing.T) {
-		opts := InstallOptions{
-			PackageType: "oops",
-		}
-		err := opts.Validate([]string{"pkg"})
-		require.Error(t, err, "Validate should have failed")
-		assert.Contains(t, err.Error(), `invalid package type "oops"`)
-	})
+	opts := InstallOptions{
+		DefaultFeedURL: "http://example.com/atom.xml", // this isn't set by the user but by the package manager
+	}
+	err := opts.Validate([]string{"pkg"})
+	require.NoError(t, err, "Validate failed")
+	assert.NotEmpty(t, opts.FeedURL, "Validate should have defaulted the feed")
+	assert.NotEmpty(t, opts.Version, "Validate should have defaulted the version")
 }
diff --git a/pkg/plugins/install_test.go b/pkg/plugins/install_test.go
index 85c15230..91a5dcaa 100644
--- a/pkg/plugins/install_test.go
+++ b/pkg/plugins/install_test.go
@@ -12,5 +12,5 @@ func TestInstallOptions_Validate(t *testing.T) {
 	opts := InstallOptions{}
 	err := opts.Validate([]string{"pkg1"})
 	require.NoError(t, err, "Validate failed")
-	assert.NotEmpty(t, opts.FeedURL, "Feed URL was not defaulted to the plugins feed URL")
+	assert.Equal(t, DefaultFeedUrl, opts.FeedURL, "Feed URL was not defaulted to the plugins feed URL")
 }
diff --git a/pkg/porter/build_test.go b/pkg/porter/build_test.go
index ff99a3c2..99b2b074 100644
--- a/pkg/porter/build_test.go
+++ b/pkg/porter/build_test.go
@@ -14,26 +14,17 @@ import (
 	"github.com/stretchr/testify/require"
 )
 
-func TestPorter_Build(t *testing.T) {
+func TestPorter_buildBundle(t *testing.T) {
 	p := NewTestPorter(t)
 
 	configTpl, err := p.Templates.GetManifest()
 	require.Nil(t, err)
 	p.TestConfig.TestContext.AddTestFileContents(configTpl, config.Name)
 
-	// Create some junk in the previous .cnab directory, build should clean it up and not copy it into the bundle
-	junkDir := ".cnab/test/junk"
-	require.NoError(t, p.FileSystem.MkdirAll(junkDir, 0755), "could not create test junk files")
-	junkExists, _ := p.FileSystem.DirExists(junkDir)
-	assert.True(t, junkExists, "failed to create junk files for the test")
-
 	err = p.LoadManifest()
 	require.NoError(t, err)
 
-	opts := BuildOptions{}
-	require.NoError(t, opts.Validate(p.Context), "Validate failed")
-
-	err = p.Build(opts)
+	err = p.buildBundle("foo", "digest")
 	require.NoError(t, err)
 
 	bundleJSONExists, err := p.FileSystem.Exists(build.LOCAL_BUNDLE)
@@ -45,10 +36,6 @@ func TestPorter_Build(t *testing.T) {
 		t.Fatalf("%s is empty", build.LOCAL_BUNDLE)
 	}
 
-	// Check that the junk files were cleaned up
-	junkExists, _ = p.FileSystem.DirExists(junkDir)
-	assert.False(t, junkExists, "junk files were not cleaned up before building")
-
 	bundleBytes, err := p.FileSystem.ReadFile(build.LOCAL_BUNDLE)
 	require.NoError(t, err)
 
diff --git a/pkg/porter/explain_test.go b/pkg/porter/explain_test.go
index bebca494..850e79db 100644
--- a/pkg/porter/explain_test.go
+++ b/pkg/porter/explain_test.go
@@ -43,7 +43,7 @@ func TestExplain_generateCredentialsTableNoCreds(t *testing.T) {
 	p := NewTestPorter(t)
 
 	p.printCredentialsExplainTable(&bun)
-	expected := "Name   Description   Required   Applies To\n"
+	expected := "Name   Description   Required\n"
 	gotOutput := p.TestConfig.TestContext.GetOutput()
 	assert.Equal(t, expected, gotOutput)
 	t.Log(gotOutput)
@@ -268,43 +268,26 @@ func TestExplain_generatePrintableBundleParamsWithAction(t *testing.T) {
 		},
 	}
 
-	t.Run("action applies", func(t *testing.T) {
-		pb, err := generatePrintable(bun, "install")
-		require.NoError(t, err)
-
-		require.Equal(t, 2, len(pb.Parameters), "expected 2 parameters")
-
-		d := pb.Parameters[0]
-		require.Equal(t, "debug", d.Name)
-		assert.Equal(t, "install", d.ApplyTo)
-
-		f := pb.Parameters[1]
-		require.Equal(t, "tfstate", f.Name)
-		assert.Equal(t, "All Actions", f.ApplyTo)
-	})
-
-	t.Run("action does not apply", func(t *testing.T) {
-		pb, err := generatePrintable(bun, "upgrade")
-		require.NoError(t, err)
-
-		require.Equal(t, 1, len(pb.Parameters), "expected only 1 parameter since debug parameter doesn't apply to upgrade command")
-		require.Equal(t, "tfstate", pb.Parameters[0].Name)
-	})
+	pb, err := generatePrintable(bun, "install")
+	require.NoError(t, err)
 
-	t.Run("all actions", func(t *testing.T) {
-		pb, err := generatePrintable(bun, "")
-		require.NoError(t, err)
+	require.Equal(t, 2, len(pb.Parameters), "expected 2 parameters")
+	d := pb.Parameters[0]
+	require.Equal(t, "debug", d.Name)
+	assert.Equal(t, "clippy", fmt.Sprintf("%v", d.Default))
+	assert.Equal(t, "string", d.Type)
+	f := pb.Parameters[1]
+	require.Equal(t, "tfstate", f.Name)
+	assert.Equal(t, "file", f.Type)
 
-		require.Equal(t, 2, len(pb.Parameters), "expected 2 parameters")
+	assert.Equal(t, 0, len(pb.Outputs))
+	assert.Equal(t, 0, len(pb.Credentials))
+	assert.Equal(t, 0, len(pb.Actions))
 
-		d := pb.Parameters[0]
-		require.Equal(t, "debug", d.Name)
-		assert.Equal(t, "install", d.ApplyTo)
+	pb2, err := generatePrintable(bun, "upgrade")
+	require.NoError(t, err)
 
-		f := pb.Parameters[1]
-		require.Equal(t, "tfstate", f.Name)
-		assert.Equal(t, "All Actions", f.ApplyTo)
-	})
+	require.Equal(t, 1, len(pb2.Parameters), "expected only 1 parameter since debug parameter doesn't apply to upgrade command")
 }
 
 func TestExplain_generatePrintableBundleOutputs(t *testing.T) {
@@ -343,13 +326,9 @@ func TestExplain_generatePrintableBundleOutputs(t *testing.T) {
 func TestExplain_generatePrintableBundleCreds(t *testing.T) {
 	bun := bundle.Bundle{
 		Credentials: map[string]bundle.Credential{
-			"kubeconfig": {
+			"debug": {
 				Required:    true,
 				Description: "a cred",
-				ApplyTo:     []string{"install"},
-			},
-			"password": {
-				Description: "another cred",
 			},
 		},
 		Custom: map[string]interface{}{
@@ -361,43 +340,16 @@ func TestExplain_generatePrintableBundleCreds(t *testing.T) {
 		},
 	}
 
-	t.Run("action applies", func(t *testing.T) {
-		pb, err := generatePrintable(bun, "install")
-		require.NoError(t, err)
-
-		require.Equal(t, 2, len(pb.Credentials), "expected 2 credentials")
-
-		d := pb.Credentials[0]
-		require.Equal(t, "kubeconfig", d.Name)
-		assert.Equal(t, "install", d.ApplyTo)
-
-		f := pb.Credentials[1]
-		require.Equal(t, "password", f.Name)
-		assert.Equal(t, "All Actions", f.ApplyTo)
-	})
-
-	t.Run("action does not apply", func(t *testing.T) {
-		pb, err := generatePrintable(bun, "upgrade")
-		require.NoError(t, err)
-
-		require.Equal(t, 1, len(pb.Credentials), "expected only 1 credential since kubeconfig credential doesn't apply to upgrade command")
-		require.Equal(t, "password", pb.Credentials[0].Name)
-	})
-
-	t.Run("all actions", func(t *testing.T) {
-		pb, err := generatePrintable(bun, "")
-		require.NoError(t, err)
-
-		require.Equal(t, 2, len(pb.Credentials), "expected 2 credentials")
-
-		d := pb.Credentials[0]
-		require.Equal(t, "kubeconfig", d.Name)
-		assert.Equal(t, "install", d.ApplyTo)
+	pb, err := generatePrintable(bun, "")
+	assert.NoError(t, err)
 
-		f := pb.Credentials[1]
-		require.Equal(t, "password", f.Name)
-		assert.Equal(t, "All Actions", f.ApplyTo)
-	})
+	assert.Equal(t, 1, len(pb.Credentials))
+	d := pb.Credentials[0]
+	assert.True(t, d.Required)
+	assert.Equal(t, "a cred", d.Description)
+	assert.Equal(t, 0, len(pb.Parameters))
+	assert.Equal(t, 0, len(pb.Outputs))
+	assert.Equal(t, 0, len(pb.Actions))
 }
 
 func TestExplain_generatePrintableBundlePorterVersion(t *testing.T) {
diff --git a/pkg/porter/packages_test.go b/pkg/porter/packages_test.go
index e8d60c98..408e1810 100644
--- a/pkg/porter/packages_test.go
+++ b/pkg/porter/packages_test.go
@@ -175,7 +175,7 @@ func TestPorter_SearchPackages_Plugins(t *testing.T) {
 // fetchFullListBytes fetches the full package list according to the
 // provided package type, sorts the list, and returns its marshaled byte form
 func fetchFullListBytes(pkgType string) ([]byte, error) {
-	url := pkgmgmt.GetPackageListURL(pkgmgmt.GetDefaultPackageMirrorURL(), pkgType)
+	url := pkgmgmt.GetPackageListURL(pkgType)
 	packageList, err := pkgmgmt.GetPackageListings(url)
 	if err != nil {
 		return nil, err
diff --git a/tests/archive_test.go b/tests/archive_test.go
index a9a5e8ce..4a5a7ef0 100644
--- a/tests/archive_test.go
+++ b/tests/archive_test.go
@@ -3,13 +3,15 @@
 package tests
 
 import (
+	"crypto/sha256"
 	"fmt"
+	"io"
 	"os"
 	"testing"
 
-	"github.com/stretchr/testify/require"
-
 	"get.porter.sh/porter/pkg/porter"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 )
 
 func TestArchive(t *testing.T) {
@@ -20,37 +22,45 @@ func TestArchive(t *testing.T) {
 	defer p.CleanupIntegrationTest()
 	p.Debug = false
 
-	bundleName := p.AddTestBundleDir("../build/testdata/bundles/mysql", true)
-	reference := fmt.Sprintf("localhost:5000/%s:v0.1.3", bundleName)
-
-	// Currently, archive requires the bundle to already be published.
-	// https://github.com/getporter/porter/issues/697
-	publishOpts := porter.PublishOptions{}
-	publishOpts.Reference = reference
-	err := publishOpts.Validate(p.Context)
-	require.NoError(p.T(), err, "validation of publish opts for bundle failed")
-
-	err = p.Publish(publishOpts)
-	require.NoError(p.T(), err, "publish of bundle failed")
+	// Use a fixed bundle to work with so that we can rely on the registry and layer digests
+	const reference = "getporter/mysql:v0.1.3"
 
 	// Archive bundle
-	archiveOpts := porter.ArchiveOptions{}
-	archiveOpts.Reference = reference
-	err = archiveOpts.Validate([]string{"mybuns.tgz"}, p.Porter)
+	archive1Opts := porter.ArchiveOptions{}
+	archive1Opts.Reference = reference
+	archiveFile1 := "mybuns1.tgz"
+	err := archive1Opts.Validate([]string{archiveFile1}, p.Porter)
 	require.NoError(p.T(), err, "validation of archive opts for bundle failed")
 
-	err = p.Archive(archiveOpts)
+	err = p.Archive(archive1Opts)
 	require.NoError(p.T(), err, "archival of bundle failed")
 
-	info, err := p.FileSystem.Stat("mybuns.tgz")
+	info, err := p.FileSystem.Stat(archiveFile1)
 	require.NoError(p.T(), err)
 	require.Equal(p.T(), os.FileMode(0644), info.Mode())
 
+	hash1 := getHash(p, archiveFile1)
+
+	// Check to be sure the shasum is stable after archiving a second time
+	archive2Opts := porter.ArchiveOptions{}
+	archive2Opts.Reference = reference
+	archiveFile2 := "mybuns2.tgz"
+	err = archive2Opts.Validate([]string{archiveFile2}, p.Porter)
+	require.NoError(p.T(), err, "validation of archive opts for bundle failed")
+
+	err = archive1Opts.Validate([]string{archiveFile2}, p.Porter)
+	require.NoError(t, err, "Second validate failed")
+
+	err = p.Archive(archive2Opts)
+	require.NoError(t, err, "Second archive failed")
+
+	assert.Equal(p.T(), hash1, getHash(p, archiveFile2), "shasum of archive did not stay the same on the second call to archive")
+
 	// Publish bundle from archive, with new reference
 	publishFromArchiveOpts := porter.PublishOptions{
-		ArchiveFile: "mybuns.tgz",
+		ArchiveFile: archiveFile1,
 		BundlePullOptions: porter.BundlePullOptions{
-			Reference: fmt.Sprintf("localhost:5000/archived-%s:v0.1.3", bundleName),
+			Reference: fmt.Sprintf("localhost:5000/archived-mysql:v0.1.3"),
 		},
 	}
 	err = publishFromArchiveOpts.Validate(p.Context)
@@ -59,3 +69,15 @@ func TestArchive(t *testing.T) {
 	err = p.Publish(publishFromArchiveOpts)
 	require.NoError(p.T(), err, "publish of bundle from archive failed")
 }
+
+func getHash(p *porter.TestPorter, path string) string {
+	f, err := p.FileSystem.Open(path)
+	require.NoError(p.T(), err, "opening archive failed")
+	defer f.Close()
+
+	h := sha256.New()
+	_, err = io.Copy(h, f)
+	require.NoError(p.T(), err, "hashing of archive failed")
+
+	return fmt.Sprintf("%x", h.Sum(nil))
+}