GoogleContainerTools/skaffold · error

URI scheme is %q, must be 'gs'

Error message

URI scheme is %q, must be 'gs'

What it means

parseGCSURI rejects URIs whose scheme is not 'gs'. Both DownloadRecursive (src) and UploadFile (dst) require Google Cloud Storage URIs of the form gs://bucket/path. This error means a non-GCS location (local path, s3://, https://, or a bare path that yields an empty scheme) was passed.

Source

Thrown at pkg/skaffold/gcs/client/native.go:139

	dstObj := urinfo.ObjPath
	if isDirectory {
		dstObj, err = url.JoinPath(dstObj, filepath.Base(src))
		if err != nil {
			return err
		}
	}

	return bucket.UploadObject(ctx, dstObj, f)
}

func (n *Native) parseGCSURI(uri string) (uriInfo, error) {
	var gcsobj uriInfo
	u, err := url.Parse(uri)
	if err != nil {
		return uriInfo{}, fmt.Errorf("cannot parse URI %q: %w", uri, err)
	}
	if u.Scheme != "gs" {
		return uriInfo{}, fmt.Errorf("URI scheme is %q, must be 'gs'", u.Scheme)
	}
	if u.Host == "" {
		return uriInfo{}, errors.New("bucket name is empty")
	}
	gcsobj.Bucket = u.Host
	// If we do this with the url package it will scape the `?` character, breaking the glob.
	gcsobj.ObjPath = strings.TrimLeft(strings.ReplaceAll(uri, "gs://"+u.Host, ""), "/")

	return gcsobj, nil
}

func (n *Native) filesToDownload(ctx context.Context, bucket bucketHandler, urinfo uriInfo) (map[string]string, error) {
	uriToLocalPath := map[string]string{}

	// The exact match is with the original glob expression. This could be:
	// 1. a/b/c -> It will return the file `c` under a/b/ if it exists
	// 2. a/b/c* -> It will return any file under a/b/ that starts with c, e.g, c1, c-other, etc
	// 3. a/b/c** -> It will return any file that starts with 'c', and files inside any folder starting with 'c'. It is doing the recursion already

View on GitHub (pinned to a1189de023)

Solutions

  1. Prefix the argument with gs:// and a bucket name, e.g. "gs://my-bucket/path/**"
  2. Check the config for an empty/missing remote path and set it correctly
  3. Use the GCS URI from `gcloud storage ls` output rather than a console web URL
  4. Add an early check that strings.HasPrefix(arg, "gs://") in your calling code for a clearer failure

Example fix

// before
n.UploadFile(ctx, "./app.tar.gz", "artifacts/app.tar.gz") // no scheme
// after
if !strings.HasPrefix("artifacts/app.tar.gz", "gs://") {
    return errors.New("destination must be a gs:// URI")
}
if err := n.UploadFile(ctx, "./app.tar.gz", "gs://artifacts/app.tar.gz"); err != nil { /* handle */ }
Defensive patterns

Strategy: validation

Validate before calling

func requireGCSURI(uri string) error {
	if !strings.HasPrefix(uri, "gs://") || len(strings.TrimPrefix(uri, "gs://")) == 0 {
		return fmt.Errorf("expected gs://bucket/path, got %q", uri)
	}
	return nil
}
// call before DownloadRecursive/UploadFile

Prevention

When it happens

Trigger: Calling DownloadRecursive with src like "/local/dir" or "s3://bucket/key", or UploadFile with a plain dst like "./remote-cache"; a bare hostname/path has empty scheme so u.Scheme != "gs" triggers this error.

Common situations: Config file mixes local and remote sources and the remote one is unset/empty; user pasted an https console URL instead of a gs:// URI; migrating from another tool that accepted s3-style URIs.

Related errors


AI-assisted analysis of GoogleContainerTools/skaffold@a1189de023 (2026-09-05). Data as JSON: /api/errors/93623ae5cdcf46bf. Report an issue: GitHub.