Repository: GoogleContainerTools/skaffold
Pull request #7769 (2022-08-10 20:30:12)

fix: add flags for build platform selection heuristics
Cross-platform and Multi-platform build uses two heuristics to determine the final target platforms:
- In `dev`, `debug` and `run` modes, it checks the active kubernetes cluster nodes and takes the intersection of that with the target platforms list.
- In `dev` and `debug` modes, it disables multi-platform builds to keep the iterative build-render-deploy loop fast.

This PR refactors these two things into flags `disable-multi-platform-build` and `check-cluster-node-platforms` with the same default behavior as before, so that they can be explicitly turned off.

**Testing instructions:**
- In the project, `integration/examples/cross-platform-builds`, run:
  ```
  skaffold build --default-repo=gcr.io/k8s-skaffold --platform=linux/amd64,linux/arm64 -t latest
  ```
  This should build and push the multi-platform image, that can be verified running:
  ```
  docker manifest inspect gcr.io/k8s-skaffold/skaffold-example:latest
  ```
  The output should be a manifest list:
  ```
  ...
  "platform": {
       "architecture": "arm64",
       "os": "linux"
  }
  ...
  "platform": {
       "architecture": "amd64",
       "os": "linux"
  }
  ```
- In the same project, set the active kubernetes context to [`linux/amd64` nodes](https://pantheon.corp.google.com/kubernetes/clusters/details/us-central1-a/integration-tests/details?project=k8s-skaffold) cluster, and run:
  ```
  skaffold dev --default-repo=gcr.io/k8s-skaffold -t latest
  ```
  Skaffold should detect the active kubernetes cluster node platform, and build for `linux/amd64`. The deploy logs should show the same:
  ```
  [getting-started] Hello world! Running on linux/amd64
  [getting-started] Hello world! Running on linux/amd64
  ```

- Now set the active kubernetes context to [`linux/arm64` nodes](https://pantheon.corp.google.com/kubernetes/clusters/details/us-central1-a/integration-tests-arm/details?project=k8s-skaffold) cluster, and run:
  ```
  skaffold dev --default-repo=gcr.io/k8s-skaffold -t latest
  ```
  Skaffold should now build for `linux/arm64`. The deploy logs should show the same:
  ```
  [getting-started] Hello world! Running on linux/arm64
  [getting-started] Hello world! Running on linux/arm64
  ```

codecov[bot]: # [Codecov](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/7769?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools) Report
> Merging [#7769](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/7769?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools) (24a1164) into [main](https://codecov.io/gh/GoogleContainerTools/skaffold/commit/290280e8c6c19231e9ed5b5c027f2c01d22f75be?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools) (290280e) will **decrease** coverage by `4.00%`.
> The diff coverage is `53.59%`.

```diff
@@            Coverage Diff             @@
##             main    #7769      +/-   ##
==========================================
- Coverage   70.48%   66.47%   -4.01%     
==========================================
  Files         515      589      +74     
  Lines       23150    28368    +5218     
==========================================
+ Hits        16317    18859    +2542     
- Misses       5776     8125    +2349     
- Partials     1057     1384     +327     
```


| [Impacted Files](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/7769?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools) | Coverage Δ | |
|---|---|---|
| [cmd/skaffold/app/cmd/credits/export.go](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/7769/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools#diff-Y21kL3NrYWZmb2xkL2FwcC9jbWQvY3JlZGl0cy9leHBvcnQuZ28=) | `0.00% <0.00%> (ø)` | |
| [cmd/skaffold/app/cmd/deploy.go](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/7769/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools#diff-Y21kL3NrYWZmb2xkL2FwcC9jbWQvZGVwbG95Lmdv) | `40.90% <0.00%> (-12.94%)` | :arrow_down: |
| [cmd/skaffold/app/cmd/test.go](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/7769/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools#diff-Y21kL3NrYWZmb2xkL2FwcC9jbWQvdGVzdC5nbw==) | `44.44% <0.00%> (ø)` | |
| [cmd/skaffold/skaffold.go](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/7769/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools#diff-Y21kL3NrYWZmb2xkL3NrYWZmb2xkLmdv) | `0.00% <0.00%> (ø)` | |
| [cmd/skaffold/app/cmd/inspect\_tests.go](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/7769/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools#diff-Y21kL3NrYWZmb2xkL2FwcC9jbWQvaW5zcGVjdF90ZXN0cy5nbw==) | `62.50% <14.28%> (-1.14%)` | :arrow_down: |
| [cmd/skaffold/app/cmd/render.go](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/7769/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools#diff-Y21kL3NrYWZmb2xkL2FwcC9jbWQvcmVuZGVyLmdv) | `35.48% <18.18%> (-5.90%)` | :arrow_down: |
| [cmd/skaffold/app/cmd/lsp.go](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/7769/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools#diff-Y21kL3NrYWZmb2xkL2FwcC9jbWQvbHNwLmdv) | `28.12% <28.12%> (ø)` | |
| [cmd/skaffold/app/cmd/run.go](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/7769/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools#diff-Y21kL3NrYWZmb2xkL2FwcC9jbWQvcnVuLmdv) | `64.28% <33.33%> (-9.63%)` | :arrow_down: |
| [cmd/skaffold/app/cmd/fix.go](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/7769/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools#diff-Y21kL3NrYWZmb2xkL2FwcC9jbWQvZml4Lmdv) | `56.41% <37.50%> (-20.07%)` | :arrow_down: |
| [cmd/skaffold/app/cmd/verify.go](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/7769/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools#diff-Y21kL3NrYWZmb2xkL2FwcC9jbWQvdmVyaWZ5Lmdv) | `41.17% <41.17%> (ø)` | |
| ... and [349 more](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/7769/diff?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools) | |

:mega: We’re building smart automated test selection to slash your CI/CD build times. [Learn more](https://about.codecov.io/iterative-testing/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools)


Patch:
diff --git a/cmd/skaffold/app/cmd/flags.go b/cmd/skaffold/app/cmd/flags.go
index ba27daea6..9bb7584fb 100644
--- a/cmd/skaffold/app/cmd/flags.go
+++ b/cmd/skaffold/app/cmd/flags.go
@@ -657,6 +657,31 @@ var flagRegistry = []Flag{
 		FlagAddMethod: "BoolVar",
 		DefinedOn:     []string{"dev", "render", "run", "debug", "deploy"},
 	},
+	{
+		Name:     "disable-multi-platform-build",
+		Usage:    "When set to true, forces only single platform image builds even when multiple target platforms are specified. Enabled by default for `dev` and `debug` modes, to keep dev-loop fast",
+		Value:    &opts.DisableMultiPlatformBuild,
+		DefValue: false,
+		DefValuePerCommand: map[string]interface{}{
+			"dev":   true,
+			"debug": true,
+		},
+		FlagAddMethod: "BoolVar",
+		DefinedOn:     []string{"build", "dev", "run", "debug"},
+	},
+	{
+		Name:     "check-cluster-node-platforms",
+		Usage:    "When set to true, images are built for the target platforms matching the active kubernetes cluster node platforms. Enabled by default for `dev`, `debug` and `run`",
+		Value:    &opts.CheckClusterNodePlatforms,
+		DefValue: false,
+		DefValuePerCommand: map[string]interface{}{
+			"dev":   true,
+			"debug": true,
+			"run":   true,
+		},
+		FlagAddMethod: "BoolVar",
+		DefinedOn:     []string{"build", "dev", "run", "debug"},
+	},
 	{
 		Name:          "cloud-run-project",
 		Shorthand:     "",
diff --git a/docs-v2/content/en/docs/pipeline-stages/builders/ko.md b/docs-v2/content/en/docs/pipeline-stages/builders/ko.md
index 86c5ac7c4..72546680b 100644
--- a/docs-v2/content/en/docs/pipeline-stages/builders/ko.md
+++ b/docs-v2/content/en/docs/pipeline-stages/builders/ko.md
@@ -389,7 +389,46 @@ To learn more about how Skaffold debugs Go applications, read the
 
 ### File sync
 
-File `sync` is not supported while the ko builder feature is in Alpha.
+The `ko` builder can
+[sync files to a running container]({{< relref "/docs/pipeline-stages/filesync" >}})
+when you run `skaffold dev`.
+
+The sync feature for the `ko` builder only works for 
+[static assets bundled with the container image](https://github.com/google/ko#static-assets).
+
+Use `infer` mode to specify patterns for the files you want to sync. The
+infer patterns are relative to the `context` directory.
+
+For instance, if your main package is in the `context` directory, you can use
+this configuration to sync all the static files bundled with the container
+image:
+
+```yaml
+    sync:
+      infer:
+      - kodata/**/*
+```
+
+Note that the file sync feature requires the `tar` command to be available in
+the container. The default `ko` builder base image does not include the `tar`
+command. Use the `fromImage` field in the `ko` builder configuration in your
+`skaffold.yaml` file to specify a base image that contains the `tar` command,
+such as `gcr.io/distroless/base:debug`.
+
+You can use [profiles]({{< relref "/docs/environment/profiles" >}}) with
+activation by command to override the `fromImage` value only when running
+`skaffold dev`, such as in this example:
+
+```yaml
+profiles:
+- name: sync
+  activation:
+  - command: dev
+  patches:
+  - op: add
+    path: /build/artifacts/0/ko/fromImage
+    value: gcr.io/distroless/base:debug
+```
 
 ### Remote builds
 
diff --git a/docs-v2/content/en/docs/pipeline-stages/filesync.md b/docs-v2/content/en/docs/pipeline-stages/filesync.md
index c641d32db..f571cc484 100644
--- a/docs-v2/content/en/docs/pipeline-stages/filesync.md
+++ b/docs-v2/content/en/docs/pipeline-stages/filesync.md
@@ -14,11 +14,12 @@ This tar file is sent to and extracted on the corresponding containers.
 Multiple types of sync are supported by Skaffold:
 
  + `manual`: The user must specify both the files in their local workspace and the destination in the running container.
-   This is supported by every type of artifact.
+   This sync mode is supported by every type of artifact.
 
  + `infer`: The destinations for each changed file is inferred from the builder.
    The docker and kaniko builders examine instructions in a Dockerfile.
    This inference is also supported for custom artifacts that **explicitly declare a dependency on a Dockerfile.**
+   The ko builder can sync static content using this sync mode.
 
 + `auto`: Skaffold automatically configures the sync.  This mode is only supported by Jib and Buildpacks artifacts.
    Auto sync mode is enabled by default for Buildpacks artifacts.
@@ -46,13 +47,18 @@ The following example showcases manual filesync:
 
 ### Inferred sync mode
 
-For docker artifacts, Skaffold knows how to infer the desired destination from the artifact's `Dockerfile`
+For Docker artifacts, Skaffold knows how to infer the desired destination from the artifact's `Dockerfile`
 by examining the `ADD` and `COPY` instructions.
-To enable syncing, you only need to specify which files are eligible for syncing in the sync rules.
-The sync rules for inferred sync mode is just a list of glob patterns.
-The following example showcases this filesync mode:
 
-Given a Dockerfile such as
+For Ko artifacts, Skaffold infers the destination from the structure of your
+codebase.
+
+To enable syncing, you specify which files are eligible for syncing in the sync rules.
+The sync rules for inferred sync mode is a list of glob patterns.
+
+The following example showcases this filesync mode for Docker artifacts:
+
+Given this Dockerfile:
 
 ```Dockerfile
 FROM hugo
@@ -74,12 +80,15 @@ And a `skaffold.yaml` with the following sync configuration:
 - The last rule enables synchronization for all `md` files below the `content/en`.
   For example, `content/en/sub/index.md` ↷ `content/sub/index.md` but _not_ `content/en_GB/index.md`.
   
-Inferred sync mode only applies to modified and added files.
-File deletion will always cause a complete rebuild.
+For Docker artifacts, inferred sync mode only applies to modified and added
+files; file deletion will cause a complete rebuild.
 
 For multi-stage Dockerfiles, Skaffold only examines the last stage.
 Use manual sync rules to sync file copies from other stages.
 
+[Ko artifacts supports syncing static content]({{<relref "/docs/pipeline-stages/builders/ko#file-sync">}}),
+and the sync rules apply to added, modified, and deleted files.
+
 ### Auto sync mode
 
 In auto sync mode, Skaffold automatically generates sync rules for known file types. 
diff --git a/docs/content/en/docs/pipeline-stages/builders/ko.md b/docs/content/en/docs/pipeline-stages/builders/ko.md
index 86c5ac7c4..72546680b 100644
--- a/docs/content/en/docs/pipeline-stages/builders/ko.md
+++ b/docs/content/en/docs/pipeline-stages/builders/ko.md
@@ -389,7 +389,46 @@ To learn more about how Skaffold debugs Go applications, read the
 
 ### File sync
 
-File `sync` is not supported while the ko builder feature is in Alpha.
+The `ko` builder can
+[sync files to a running container]({{< relref "/docs/pipeline-stages/filesync" >}})
+when you run `skaffold dev`.
+
+The sync feature for the `ko` builder only works for 
+[static assets bundled with the container image](https://github.com/google/ko#static-assets).
+
+Use `infer` mode to specify patterns for the files you want to sync. The
+infer patterns are relative to the `context` directory.
+
+For instance, if your main package is in the `context` directory, you can use
+this configuration to sync all the static files bundled with the container
+image:
+
+```yaml
+    sync:
+      infer:
+      - kodata/**/*
+```
+
+Note that the file sync feature requires the `tar` command to be available in
+the container. The default `ko` builder base image does not include the `tar`
+command. Use the `fromImage` field in the `ko` builder configuration in your
+`skaffold.yaml` file to specify a base image that contains the `tar` command,
+such as `gcr.io/distroless/base:debug`.
+
+You can use [profiles]({{< relref "/docs/environment/profiles" >}}) with
+activation by command to override the `fromImage` value only when running
+`skaffold dev`, such as in this example:
+
+```yaml
+profiles:
+- name: sync
+  activation:
+  - command: dev
+  patches:
+  - op: add
+    path: /build/artifacts/0/ko/fromImage
+    value: gcr.io/distroless/base:debug
+```
 
 ### Remote builds
 
diff --git a/docs/content/en/docs/pipeline-stages/filesync.md b/docs/content/en/docs/pipeline-stages/filesync.md
index c641d32db..f571cc484 100644
--- a/docs/content/en/docs/pipeline-stages/filesync.md
+++ b/docs/content/en/docs/pipeline-stages/filesync.md
@@ -14,11 +14,12 @@ This tar file is sent to and extracted on the corresponding containers.
 Multiple types of sync are supported by Skaffold:
 
  + `manual`: The user must specify both the files in their local workspace and the destination in the running container.
-   This is supported by every type of artifact.
+   This sync mode is supported by every type of artifact.
 
  + `infer`: The destinations for each changed file is inferred from the builder.
    The docker and kaniko builders examine instructions in a Dockerfile.
    This inference is also supported for custom artifacts that **explicitly declare a dependency on a Dockerfile.**
+   The ko builder can sync static content using this sync mode.
 
 + `auto`: Skaffold automatically configures the sync.  This mode is only supported by Jib and Buildpacks artifacts.
    Auto sync mode is enabled by default for Buildpacks artifacts.
@@ -46,13 +47,18 @@ The following example showcases manual filesync:
 
 ### Inferred sync mode
 
-For docker artifacts, Skaffold knows how to infer the desired destination from the artifact's `Dockerfile`
+For Docker artifacts, Skaffold knows how to infer the desired destination from the artifact's `Dockerfile`
 by examining the `ADD` and `COPY` instructions.
-To enable syncing, you only need to specify which files are eligible for syncing in the sync rules.
-The sync rules for inferred sync mode is just a list of glob patterns.
-The following example showcases this filesync mode:
 
-Given a Dockerfile such as
+For Ko artifacts, Skaffold infers the destination from the structure of your
+codebase.
+
+To enable syncing, you specify which files are eligible for syncing in the sync rules.
+The sync rules for inferred sync mode is a list of glob patterns.
+
+The following example showcases this filesync mode for Docker artifacts:
+
+Given this Dockerfile:
 
 ```Dockerfile
 FROM hugo
@@ -74,12 +80,15 @@ And a `skaffold.yaml` with the following sync configuration:
 - The last rule enables synchronization for all `md` files below the `content/en`.
   For example, `content/en/sub/index.md` ↷ `content/sub/index.md` but _not_ `content/en_GB/index.md`.
   
-Inferred sync mode only applies to modified and added files.
-File deletion will always cause a complete rebuild.
+For Docker artifacts, inferred sync mode only applies to modified and added
+files; file deletion will cause a complete rebuild.
 
 For multi-stage Dockerfiles, Skaffold only examines the last stage.
 Use manual sync rules to sync file copies from other stages.
 
+[Ko artifacts supports syncing static content]({{<relref "/docs/pipeline-stages/builders/ko#file-sync">}}),
+and the sync rules apply to added, modified, and deleted files.
+
 ### Auto sync mode
 
 In auto sync mode, Skaffold automatically generates sync rules for known file types. 
diff --git a/docs/content/en/docs/references/cli/_index.md b/docs/content/en/docs/references/cli/_index.md
index 142526fcb..1569db194 100644
--- a/docs/content/en/docs/references/cli/_index.md
+++ b/docs/content/en/docs/references/cli/_index.md
@@ -198,9 +198,11 @@ Options:
   -b, --build-image=[]: Only build artifacts with image names that contain the given substring. Default is to build sources for all artifacts
       --cache-artifacts=true: Set to false to disable default caching of artifacts
       --cache-file='': Specify the location of the cache file (default $HOME/.skaffold/cache)
+      --check-cluster-node-platforms=false: When set to true, images are built for the target platforms matching the active kubernetes cluster node platforms. Enabled by default for `dev`, `debug` and `run`
   -c, --config='': File for global configurations (defaults to $HOME/.skaffold/config)
   -d, --default-repo='': Default repository value (overrides global config)
       --detect-minikube=true: Use heuristics to detect a minikube cluster
+      --disable-multi-platform-build=false: When set to true, forces only single platform image builds even when multiple target platforms are specified. Enabled by default for `dev` and `debug` modes, to keep dev-loop fast
       --dry-run=false: Don't build images, just compute the tag for each artifact.
       --file-output='': Filename to write build images to
   -f, --filename='skaffold.yaml': Path or URL to the Skaffold config file
@@ -240,9 +242,11 @@ Env vars:
 * `SKAFFOLD_BUILD_IMAGE` (same as `--build-image`)
 * `SKAFFOLD_CACHE_ARTIFACTS` (same as `--cache-artifacts`)
 * `SKAFFOLD_CACHE_FILE` (same as `--cache-file`)
+* `SKAFFOLD_CHECK_CLUSTER_NODE_PLATFORMS` (same as `--check-cluster-node-platforms`)
 * `SKAFFOLD_CONFIG` (same as `--config`)
 * `SKAFFOLD_DEFAULT_REPO` (same as `--default-repo`)
 * `SKAFFOLD_DETECT_MINIKUBE` (same as `--detect-minikube`)
+* `SKAFFOLD_DISABLE_MULTI_PLATFORM_BUILD` (same as `--disable-multi-platform-build`)
 * `SKAFFOLD_DRY_RUN` (same as `--dry-run`)
 * `SKAFFOLD_FILE_OUTPUT` (same as `--file-output`)
 * `SKAFFOLD_FILENAME` (same as `--filename`)
@@ -408,12 +412,14 @@ Options:
       --build-concurrency=-1: Number of concurrently running builds. Set to 0 to run all builds in parallel. Doesn't violate build order among dependencies.
       --cache-artifacts=true: Set to false to disable default caching of artifacts
       --cache-file='': Specify the location of the cache file (default $HOME/.skaffold/cache)
+      --check-cluster-node-platforms=true: When set to true, images are built for the target platforms matching the active kubernetes cluster node platforms. Enabled by default for `dev`, `debug` and `run`
       --cleanup=true: Delete deployments after dev or debug mode is interrupted
       --cloud-run-location='': The GCP Region to deploy Cloud Run services to
       --cloud-run-project='': The GCP Project ID or Project Number to deploy for Cloud Run
   -c, --config='': File for global configurations (defaults to $HOME/.skaffold/config)
   -d, --default-repo='': Default repository value (overrides global config)
       --detect-minikube=true: Use heuristics to detect a minikube cluster
+      --disable-multi-platform-build=true: When set to true, forces only single platform image builds even when multiple target platforms are specified. Enabled by default for `dev` and `debug` modes, to keep dev-loop fast
       --enable-platform-node-affinity=true: If true, when deploying to a mixed node cluster, skaffold will add platform (os/arch) node affinity definition to rendered manifests based on the image platforms
   -f, --filename='skaffold.yaml': Path or URL to the Skaffold config file
       --force=false: Recreate Kubernetes resources if necessary for deployment, warning: might cause downtime!
@@ -469,12 +475,14 @@ Env vars:
 * `SKAFFOLD_BUILD_CONCURRENCY` (same as `--build-concurrency`)
 * `SKAFFOLD_CACHE_ARTIFACTS` (same as `--cache-artifacts`)
 * `SKAFFOLD_CACHE_FILE` (same as `--cache-file`)
+* `SKAFFOLD_CHECK_CLUSTER_NODE_PLATFORMS` (same as `--check-cluster-node-platforms`)
 * `SKAFFOLD_CLEANUP` (same as `--cleanup`)
 * `SKAFFOLD_CLOUD_RUN_LOCATION` (same as `--cloud-run-location`)
 * `SKAFFOLD_CLOUD_RUN_PROJECT` (same as `--cloud-run-project`)
 * `SKAFFOLD_CONFIG` (same as `--config`)
 * `SKAFFOLD_DEFAULT_REPO` (same as `--default-repo`)
 * `SKAFFOLD_DETECT_MINIKUBE` (same as `--detect-minikube`)
+* `SKAFFOLD_DISABLE_MULTI_PLATFORM_BUILD` (same as `--disable-multi-platform-build`)
 * `SKAFFOLD_ENABLE_PLATFORM_NODE_AFFINITY` (same as `--enable-platform-node-affinity`)
 * `SKAFFOLD_FILENAME` (same as `--filename`)
 * `SKAFFOLD_FORCE` (same as `--force`)
@@ -693,6 +701,7 @@ Options:
       --build-concurrency=-1: Number of concurrently running builds. Set to 0 to run all builds in parallel. Doesn't violate build order among dependencies.
       --cache-artifacts=true: Set to false to disable default caching of artifacts
       --cache-file='': Specify the location of the cache file (default $HOME/.skaffold/cache)
+      --check-cluster-node-platforms=true: When set to true, images are built for the target platforms matching the active kubernetes cluster node platforms. Enabled by default for `dev`, `debug` and `run`
       --cleanup=true: Delete deployments after dev or debug mode is interrupted
       --cloud-run-location='': The GCP Region to deploy Cloud Run services to
       --cloud-run-project='': The GCP Project ID or Project Number to deploy for Cloud Run
@@ -700,6 +709,7 @@ Options:
   -d, --default-repo='': Default repository value (overrides global config)
       --detect-minikube=true: Use heuristics to detect a minikube cluster
       --digest-source='': Set to 'remote' to skip builds and resolve the digest of images by tag from the remote registry. Set to 'local' to build images locally and use digests from built images. Set to 'tag' to use tags directly from the build. Set to 'none' to use tags directly from the Kubernetes manifests. If unspecified, defaults to 'remote' for remote clusters, and 'tag' for local clusters like kind or minikube.
+      --disable-multi-platform-build=true: When set to true, forces only single platform image builds even when multiple target platforms are specified. Enabled by default for `dev` and `debug` modes, to keep dev-loop fast
       --enable-platform-node-affinity=true: If true, when deploying to a mixed node cluster, skaffold will add platform (os/arch) node affinity definition to rendered manifests based on the image platforms
   -f, --filename='skaffold.yaml': Path or URL to the Skaffold config file
       --force=false: Recreate Kubernetes resources if necessary for deployment, warning: might cause downtime!
@@ -754,6 +764,7 @@ Env vars:
 * `SKAFFOLD_BUILD_CONCURRENCY` (same as `--build-concurrency`)
 * `SKAFFOLD_CACHE_ARTIFACTS` (same as `--cache-artifacts`)
 * `SKAFFOLD_CACHE_FILE` (same as `--cache-file`)
+* `SKAFFOLD_CHECK_CLUSTER_NODE_PLATFORMS` (same as `--check-cluster-node-platforms`)
 * `SKAFFOLD_CLEANUP` (same as `--cleanup`)
 * `SKAFFOLD_CLOUD_RUN_LOCATION` (same as `--cloud-run-location`)
 * `SKAFFOLD_CLOUD_RUN_PROJECT` (same as `--cloud-run-project`)
@@ -761,6 +772,7 @@ Env vars:
 * `SKAFFOLD_DEFAULT_REPO` (same as `--default-repo`)
 * `SKAFFOLD_DETECT_MINIKUBE` (same as `--detect-minikube`)
 * `SKAFFOLD_DIGEST_SOURCE` (same as `--digest-source`)
+* `SKAFFOLD_DISABLE_MULTI_PLATFORM_BUILD` (same as `--disable-multi-platform-build`)
 * `SKAFFOLD_ENABLE_PLATFORM_NODE_AFFINITY` (same as `--enable-platform-node-affinity`)
 * `SKAFFOLD_FILENAME` (same as `--filename`)
 * `SKAFFOLD_FORCE` (same as `--force`)
@@ -1042,6 +1054,7 @@ Options:
   -b, --build-image=[]: Only build artifacts with image names that contain the given substring. Default is to build sources for all artifacts
       --cache-artifacts=true: Set to false to disable default caching of artifacts
       --cache-file='': Specify the location of the cache file (default $HOME/.skaffold/cache)
+      --check-cluster-node-platforms=true: When set to true, images are built for the target platforms matching the active kubernetes cluster node platforms. Enabled by default for `dev`, `debug` and `run`
       --cleanup=true: Delete deployments after dev or debug mode is interrupted
       --cloud-run-location='': The GCP Region to deploy Cloud Run services to
       --cloud-run-project='': The GCP Project ID or Project Number to deploy for Cloud Run
@@ -1049,6 +1062,7 @@ Options:
   -d, --default-repo='': Default repository value (overrides global config)
       --detect-minikube=true: Use heuristics to detect a minikube cluster
       --digest-source='': Set to 'remote' to skip builds and resolve the digest of images by tag from the remote registry. Set to 'local' to build images locally and use digests from built images. Set to 'tag' to use tags directly from the build. Set to 'none' to use tags directly from the Kubernetes manifests. If unspecified, defaults to 'remote' for remote clusters, and 'tag' for local clusters like kind or minikube.
+      --disable-multi-platform-build=false: When set to true, forces only single platform image builds even when multiple target platforms are specified. Enabled by default for `dev` and `debug` modes, to keep dev-loop fast
       --enable-platform-node-affinity=true: If true, when deploying to a mixed node cluster, skaffold will add platform (os/arch) node affinity definition to rendered manifests based on the image platforms
   -f, --filename='skaffold.yaml': Path or URL to the Skaffold config file
       --force=false: Recreate Kubernetes resources if necessary for deployment, warning: might cause downtime!
@@ -1098,6 +1112,7 @@ Env vars:
 * `SKAFFOLD_BUILD_IMAGE` (same as `--build-image`)
 * `SKAFFOLD_CACHE_ARTIFACTS` (same as `--cache-artifacts`)
 * `SKAFFOLD_CACHE_FILE` (same as `--cache-file`)
+* `SKAFFOLD_CHECK_CLUSTER_NODE_PLATFORMS` (same as `--check-cluster-node-platforms`)
 * `SKAFFOLD_CLEANUP` (same as `--cleanup`)
 * `SKAFFOLD_CLOUD_RUN_LOCATION` (same as `--cloud-run-location`)
 * `SKAFFOLD_CLOUD_RUN_PROJECT` (same as `--cloud-run-project`)
@@ -1105,6 +1120,7 @@ Env vars:
 * `SKAFFOLD_DEFAULT_REPO` (same as `--default-repo`)
 * `SKAFFOLD_DETECT_MINIKUBE` (same as `--detect-minikube`)
 * `SKAFFOLD_DIGEST_SOURCE` (same as `--digest-source`)
+* `SKAFFOLD_DISABLE_MULTI_PLATFORM_BUILD` (same as `--disable-multi-platform-build`)
 * `SKAFFOLD_ENABLE_PLATFORM_NODE_AFFINITY` (same as `--enable-platform-node-affinity`)
 * `SKAFFOLD_FILENAME` (same as `--filename`)
 * `SKAFFOLD_FORCE` (same as `--force`)
diff --git a/integration/examples/ko-sync-infer/README.md b/integration/examples/ko-sync-infer/README.md
new file mode 100644
index 000000000..841a7fec8
--- /dev/null
+++ b/integration/examples/ko-sync-infer/README.md
@@ -0,0 +1,18 @@
+### Example: inferred file sync using the ko builder
+
+This example uses
+[inferred file sync](https://skaffold.dev/docs/pipeline-stages/filesync/#inferred-sync-mode)
+for static assets with the
+[`ko` builder](https://skaffold.dev/docs/pipeline-stages/builders/ko/)
+for a Go web app.
+
+To observe the behavior of file sync, run this command:
+
+```shell
+skaffold dev
+```
+
+Try changing the HTML file in the `kodata` directory to see how Skaffold
+syncs the file.
+
+If change the the `main.go` file, Skaffold will rebuild and redeploy the image.
diff --git a/integration/examples/ko-sync-infer/go.mod b/integration/examples/ko-sync-infer/go.mod
new file mode 100644
index 000000000..00bfaad19
--- /dev/null
+++ b/integration/examples/ko-sync-infer/go.mod
@@ -0,0 +1,17 @@
+// Copyright 2022 The Skaffold Authors
+//
+// 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.
+
+module github.com/GoogleContainerTools/skaffold/examples/ko-sync-infer
+
+go 1.13
diff --git a/integration/examples/ko-sync-infer/k8s/example.yaml b/integration/examples/ko-sync-infer/k8s/example.yaml
new file mode 100644
index 000000000..58fd8e043
--- /dev/null
+++ b/integration/examples/ko-sync-infer/k8s/example.yaml
@@ -0,0 +1,45 @@
+# Copyright 2022 The Skaffold Authors
+#
+# 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.
+
+apiVersion: v1
+kind: Service
+metadata:
+  name: ko-sync-infer
+spec:
+  ports:
+  - name: http
+    port: 80
+    targetPort: 8080
+  selector:
+    app: ko-sync-infer
+  type: ClusterIP
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: ko-sync-infer
+spec:
+  selector:
+    matchLabels:
+      app: ko-sync-infer
+  template:
+    metadata:
+      labels:
+        app: ko-sync-infer
+    spec:
+      containers:
+      - image: skaffold-ko-sync-infer
+        name: ko-sync-infer
+        ports:
+        - containerPort: 8080
diff --git a/integration/examples/ko-sync-infer/kodata/index.html b/integration/examples/ko-sync-infer/kodata/index.html
new file mode 100644
index 000000000..d87458dc8
--- /dev/null
+++ b/integration/examples/ko-sync-infer/kodata/index.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<!--
+Copyright 2022 The Skaffold Authors
+
+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.
+-->
+<html lang="en">
+<head>
+  <meta charset="utf-8">
+  <meta name="viewport" content="width=device-width">
+  <title>Skaffold ko builder inferred file sync example</title>
+  <link rel="icon" href="data:,">
+</head>
+<body>
+  <p>Try changing this text in dev mode.</p>
+</body>
+</html>
diff --git a/integration/examples/ko-sync-infer/main.go b/integration/examples/ko-sync-infer/main.go
new file mode 100644
index 000000000..2699b16a5
--- /dev/null
+++ b/integration/examples/ko-sync-infer/main.go
@@ -0,0 +1,43 @@
+/*
+Copyright 2022 The Skaffold Authors
+
+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 main
+
+import (
+	"fmt"
+	"log"
+	"net/http"
+	"os"
+)
+
+const defaultStaticPath = "kodata" // the value to use for local development
+
+func main() {
+	if err := run(); err != nil {
+		fmt.Fprintf(os.Stderr, "%+v", err)
+		os.Exit(1)
+	}
+}
+
+func run() error {
+	staticPath := os.Getenv("KO_DATA_PATH")
+	if staticPath == "" {
+		staticPath = defaultStaticPath
+	}
+	http.Handle("/", http.FileServer(http.Dir(staticPath)))
+	log.Println("Listening on port 8080")
+	return http.ListenAndServe(":8080", nil)
+}
diff --git a/integration/examples/ko-sync-infer/skaffold.yaml b/integration/examples/ko-sync-infer/skaffold.yaml
new file mode 100644
index 000000000..503056c38
--- /dev/null
+++ b/integration/examples/ko-sync-infer/skaffold.yaml
@@ -0,0 +1,42 @@
+# Copyright 2022 The Skaffold Authors
+#
+# 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.
+
+apiVersion: skaffold/v3alpha1
+kind: Config
+build:
+  artifacts:
+  - image: skaffold-ko-sync-infer
+    ko:
+      dependencies:
+        paths:
+        - "**/*.go"
+        - go.*
+        - kodata/**/*
+    sync:
+      infer:
+      - kodata/**/*
+portForward:
+- resourceType: Deployment
+  resourceName: ko-sync-infer
+  port: 8080
+  localPort: 8080
+# override the default base image with one that supports file sync when running 'skaffold dev'
+profiles:
+- name: sync
+  activation:
+  - command: dev
+  patches:
+  - op: add
+    path: /build/artifacts/0/ko/fromImage
+    value: gcr.io/distroless/base:debug
diff --git a/pkg/skaffold/build/ko/sync/sync.go b/pkg/skaffold/build/ko/sync/sync.go
new file mode 100644
index 000000000..42fc0d557
--- /dev/null
+++ b/pkg/skaffold/build/ko/sync/sync.go
@@ -0,0 +1,108 @@
+/*
+Copyright 2022 The Skaffold Authors
+
+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 sync
+
+import (
+	"context"
+	"fmt"
+	"path/filepath"
+	"strings"
+
+	"github.com/bmatcuk/doublestar"
+
+	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/filemon"
+	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/output/log"
+	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/schema/latest"
+)
+
+// kodataRoot is the directory in the container where ko places static assets.
+// See https://github.com/google/ko/blob/2f230b88c4891ee3a71b01c1fa65e85e8d6b5f5b/README.md#static-assets
+// and https://github.com/google/ko/blob/2f230b88c4891ee3a71b01c1fa65e85e8d6b5f5b/pkg/build/gobuild.go#L514
+const kodataRoot = "/var/run/ko"
+
+// Infer syncs static content in the kodata directory based on matching file name patterns.
+// It returns maps of files to be copied and deleted.
+func Infer(ctx context.Context, a *latest.Artifact, e filemon.Events) (toCopy map[string][]string, toDelete map[string][]string, err error) {
+	toCopy, err = inferSync(ctx, a, append(e.Modified, e.Added...))
+	if err != nil {
+		return nil, nil, err
+	}
+	toDelete, err = inferSync(ctx, a, e.Deleted)
+	if err != nil {
+		return nil, nil, err
+	}
+	return
+}
+
+// inferSync determines if the files match any of the inferred file sync patterns configured for the artifact.
+// For files that matches at least one pattern, the function determines the destination path.
+// The return value is a map of source file location to destination path.
+func inferSync(ctx context.Context, a *latest.Artifact, files []string) (map[string][]string, error) {
+	localBasePath, err := findLocalKodataPath(a)
+	if err != nil {
+		return nil, err
+	}
+	toSync := map[string][]string{}
+	for _, f := range files {
+		dest, err := syncDest(f, a.Workspace, localBasePath, a.Sync.Infer)
+		if err != nil {
+			return nil, err
+		}
+		if dest != "" {
+			log.Entry(ctx).Debugf("Syncing %q to %q", f, dest)
+			toSync[f] = []string{dest}
+		} else {
+			log.Entry(ctx).Debugf("File %q does not match any sync pattern. Skipping sync", f)
+		}
+	}
+	return toSync, nil
+}
+
+// syncDest returns the destination file paths if the input file path matches at least one of the patterns.
+// If the file doesn't match any of the patterns, the function returns zero values.
+func syncDest(f string, workspace string, localBasePath string, patterns []string) (string, error) {
+	relPath, err := filepath.Rel(workspace, f)
+	if err != nil {
+		return "", err
+	}
+	for _, p := range patterns {
+		matches, err := doublestar.PathMatch(filepath.FromSlash(p), relPath)
+		if err != nil {
+			return "", fmt.Errorf("pattern error for file %q and pattern %s: %w", relPath, p, err)
+		}
+		if matches {
+			// find path to file relative to local static file directory
+			localFile, err := filepath.Rel(localBasePath, f)
+			if err != nil {
+				return "", fmt.Errorf("relative path error for path %q and file %q: %w", localBasePath, f, err)
+			}
+			dest := strings.ReplaceAll(filepath.Join(kodataRoot, localFile), "\\", "/")
+			return dest, nil
+		}
+	}
+	return "", nil
+}
+
+// findLocalKodataPath returns the local path to static content for ko artifacts.
+func findLocalKodataPath(a *latest.Artifact) (string, error) {
+	if strings.Contains(a.KoArtifact.Main, "...") {
+		// this error should be caught by validation earlier
+		return "", fmt.Errorf("unable to infer file sync when ko.main contains the '...' wildcard")
+	}
+	path := filepath.Join(a.Workspace, a.KoArtifact.Dir, a.KoArtifact.Main, "kodata")
+	return path, nil
+}
diff --git a/pkg/skaffold/config/options.go b/pkg/skaffold/config/options.go
index 6e8988b04..6a6036fc9 100644
--- a/pkg/skaffold/config/options.go
+++ b/pkg/skaffold/config/options.go
@@ -59,6 +59,8 @@ type SkaffoldOptions struct {
 	Tail                       bool
 	WaitForConnection          bool
 	EnablePlatformNodeAffinity bool
+	DisableMultiPlatformBuild  bool
+	CheckClusterNodePlatforms  bool
 	MakePathsAbsolute          *bool
 	MultiLevelRepo             *bool
 	CloudRunProject            string
diff --git a/pkg/skaffold/docker/manifest_list.go b/pkg/skaffold/docker/manifest_list.go
index 0b140bb64..27de3b1f3 100644
--- a/pkg/skaffold/docker/manifest_list.go
+++ b/pkg/skaffold/docker/manifest_list.go
@@ -30,6 +30,12 @@ import (
 	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/output/log"
 )
 
+// for testing
+var (
+	mutateAppendManifest = mutate.AppendManifests
+	remoteWriteIndex     = remote.WriteIndex
+)
+
 // Describes the result of an image build.
 type SinglePlatformImage struct {
 	// Platform (OS + architecture) associated with the image built.
@@ -49,7 +55,7 @@ func CreateManifestList(ctx context.Context, images []SinglePlatformImage, targe
 			return "", err
 		}
 
-		img, err := remote.Image(ref)
+		img, err := remoteImage(ref)
 		if err != nil {
 			return "", err
 		}
@@ -61,13 +67,13 @@ func CreateManifestList(ctx context.Context, images []SinglePlatformImage, targe
 			},
 		}
 	}
-	idx := mutate.AppendManifests(mutate.IndexMediaType(empty.Index, types.DockerManifestList), adds...)
+	idx := mutateAppendManifest(mutate.IndexMediaType(empty.Index, types.DockerManifestList), adds...)
 	targetRef, err := name.ParseReference(targetTag, name.WeakValidation)
 	if err != nil {
 		return "", err
 	}
 
-	err = remote.WriteIndex(targetRef, idx, remote.WithAuthFromKeychain(primaryKeychain))
+	err = remoteWriteIndex(targetRef, idx, remote.WithAuthFromKeychain(primaryKeychain))
 	if err != nil {
 		return "", err
 	}
diff --git a/pkg/skaffold/platform/resolver.go b/pkg/skaffold/platform/resolver.go
index 49c476f7f..0ee879033 100644
--- a/pkg/skaffold/platform/resolver.go
+++ b/pkg/skaffold/platform/resolver.go
@@ -24,7 +24,6 @@ import (
 	v1 "github.com/opencontainers/image-spec/specs-go/v1"
 	coreV1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 
-	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/config"
 	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/instrumentation"
 	kubernetesclient "github.com/GoogleContainerTools/skaffold/pkg/skaffold/kubernetes/client"
 	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/output/log"
@@ -41,6 +40,13 @@ type Resolver struct {
 	platformsByImageName map[string]Matcher
 }
 
+type ResolverOpts struct {
+	KubeContext               string
+	CliPlatformsSelection     []string
+	DisableMultiPlatformBuild bool
+	CheckClusterNodePlatforms bool
+}
+
 func (r Resolver) GetPlatforms(imageName string) Matcher {
 	if r.platformsByImageName == nil {
 		return Matcher{}
@@ -48,28 +54,31 @@ func (r Resolver) GetPlatforms(imageName string) Matcher {
 	return r.platformsByImageName[imageName]
 }
 
-func NewResolver(ctx context.Context, pipelines []latest.Pipeline, cliPlatformsSelection []string, runMode config.RunMode, kubeContext string) (Resolver, error) {
+func NewResolver(ctx context.Context, pipelines []latest.Pipeline, opts ResolverOpts) (Resolver, error) {
 	r := Resolver{}
 	r.platformsByImageName = make(map[string]Matcher)
 
 	var fromCli, fromClusterNodes Matcher
 	var err error
-	fromCli, err = Parse(cliPlatformsSelection)
+	fromCli, err = Parse(opts.CliPlatformsSelection)
 	if err != nil {
 		return r, fmt.Errorf("failed to parse platforms: %w", err)
 	}
 	log.Entry(ctx).Debugf("CLI platforms provided: %q", fromCli)
 	instrumentation.AddCliBuildTargetPlatforms(fromCli.String())
 
-	if runMode == config.RunModes.Dev || runMode == config.RunModes.Debug || runMode == config.RunModes.Run {
-		fromClusterNodes, err = getClusterPlatforms(ctx, kubeContext, runMode == config.RunModes.Dev || runMode == config.RunModes.Debug)
+	if opts.CheckClusterNodePlatforms {
+		fromClusterNodes, err = getClusterPlatforms(ctx, opts.KubeContext)
 		if err != nil {
 			log.Entry(ctx).Debugf("failed to get cluster node details: %v", err)
 			log.Entry(ctx).Warnln("failed to detect active kubernetes cluster node platform. Specify the correct build platform in the `skaffold.yaml` file or using the `--platform` flag")
 		}
+		log.Entry(ctx).Debugf("platforms detected from active kubernetes cluster nodes: %q", fromClusterNodes)
+		instrumentation.AddDeployNodePlatforms(fromClusterNodes.String())
+	} else {
+		log.Entry(ctx).Debugln("platform detection from active kubernetes cluster is not enabled")
 	}
-	log.Entry(ctx).Debugf("platforms detected from active kubernetes cluster nodes: %q", fromClusterNodes)
-	instrumentation.AddDeployNodePlatforms(fromClusterNodes.String())
+
 	for _, pipeline := range pipelines {
 		platforms := fromCli
 		if platforms.IsEmpty() {
@@ -101,7 +110,9 @@ func NewResolver(ctx context.Context, pipelines []latest.Pipeline, cliPlatformsS
 					return r, fmt.Errorf("build target platforms %q do not match platform constraints %q defined for artifact %q", platforms, artifact.Platforms, artifact.ImageName)
 				}
 			}
-
+			if pl.IsMultiPlatform() && opts.DisableMultiPlatformBuild {
+				pl = selectOnePlatform(pl)
+			}
 			r.platformsByImageName[artifact.ImageName] = pl
 			log.Entry(ctx).Debugf("platforms selected for artifact %q: %q", artifact.ImageName, pl)
 		}
@@ -110,8 +121,7 @@ func NewResolver(ctx context.Context, pipelines []latest.Pipeline, cliPlatformsS
 }
 
 // GetClusterPlatforms returns the platforms for the active kubernetes cluster.
-// For `dev` or `debug` if there are multiple cluster node platforms, then it returns only one selection, preferably matching the host platform.
-func GetClusterPlatforms(ctx context.Context, kContext string, isDevOrDebug bool) (Matcher, error) {
+func GetClusterPlatforms(ctx context.Context, kContext string) (Matcher, error) {
 	client, err := kubernetesclient.Client(kContext)
 	if err != nil {
 		return Matcher{}, fmt.Errorf("failed to determine kubernetes cluster node platforms: %w", err)
@@ -137,18 +147,18 @@ func GetClusterPlatforms(ctx context.Context, kContext string, isDevOrDebug bool
 	for _, key := range keys {
 		m.Platforms = append(m.Platforms, set[key])
 	}
+	return m, nil
+}
 
-	if !isDevOrDebug || len(m.Platforms) <= 1 {
-		return m, nil
-	}
-
-	// If there's more than 1 node platform type and we're running `dev` or `debug`,
-	// then select the platform type that matches the host platform.
-	// If there's no match then just return the first node platform type found.
+func selectOnePlatform(m Matcher) Matcher {
+	// Select the platform type that matches the host platform if possible.
 	filtered := m.Intersect(getHostMatcher())
-	if len(filtered.Platforms) == 0 {
-		m.Platforms = m.Platforms[0:1] // guaranteed to have at least 1 element
-		return m, nil
+	if len(filtered.Platforms) != 0 {
+		return filtered
+	}
+	if len(m.Platforms) > 1 {
+		// If there's no match then just return the first node platform type found.
+		m.Platforms = m.Platforms[0:1]
 	}
-	return filtered, nil
+	return m
 }
diff --git a/pkg/skaffold/runner/runcontext/v2/context.go b/pkg/skaffold/runner/runcontext/v2/context.go
index d55d4043f..de733f2da 100644
--- a/pkg/skaffold/runner/runcontext/v2/context.go
+++ b/pkg/skaffold/runner/runcontext/v2/context.go
@@ -232,6 +232,8 @@ func (rc *RunContext) GetWorkingDir() string                         { return rc
 func (rc *RunContext) GetCluster() config.Cluster                    { return rc.Cluster }
 func (rc *RunContext) GetNamespace() string                          { return rc.Opts.Namespace }
 func (rc *RunContext) AutoBuild() bool                               { return rc.Opts.AutoBuild }
+func (rc *RunContext) DisableMultiPlatformBuild() bool               { return rc.Opts.DisableMultiPlatformBuild }
+func (rc *RunContext) CheckClusterNodePlatforms() bool               { return rc.Opts.CheckClusterNodePlatforms }
 func (rc *RunContext) AutoDeploy() bool                              { return rc.Opts.AutoDeploy }
 func (rc *RunContext) AutoSync() bool                                { return rc.Opts.AutoSync }
 func (rc *RunContext) ContainerDebugging() bool                      { return rc.Opts.ContainerDebugging }
diff --git a/pkg/skaffold/runner/v2/new.go b/pkg/skaffold/runner/v2/new.go
index cc3b69367..c2f5e846b 100644
--- a/pkg/skaffold/runner/v2/new.go
+++ b/pkg/skaffold/runner/v2/new.go
@@ -92,7 +92,14 @@ func NewForConfig(ctx context.Context, runCtx *runcontext.RunContext) (*Skaffold
 		endTrace(instrumentation.TraceEndError(err))
 		return nil, fmt.Errorf("creating deployer: %w", err)
 	}
-	platforms, err := platform.NewResolver(ctx, runCtx.Pipelines.All(), runCtx.Opts.Platforms, runCtx.Mode(), runCtx.KubeContext)
+	rOpts := platform.ResolverOpts{
+		KubeContext:               runCtx.KubeContext,
+		CliPlatformsSelection:     runCtx.Opts.Platforms,
+		CheckClusterNodePlatforms: runCtx.CheckClusterNodePlatforms(),
+		DisableMultiPlatformBuild: runCtx.DisableMultiPlatformBuild(),
+	}
+
+	platforms, err := platform.NewResolver(ctx, runCtx.Pipelines.All(), rOpts)
 	if err != nil {
 		endTrace(instrumentation.TraceEndError(err))
 		return nil, fmt.Errorf("getting target platforms: %w", err)
diff --git a/pkg/skaffold/schema/validation/validation.go b/pkg/skaffold/schema/validation/validation.go
index b70e3a4c5..58ac6e1fd 100644
--- a/pkg/skaffold/schema/validation/validation.go
+++ b/pkg/skaffold/schema/validation/validation.go
@@ -80,6 +80,7 @@ func ProcessToErrorWithLocation(configs parser.SkaffoldConfigSet, validateConfig
 		errs = append(errs, validateSyncRules(config, config.Build.Artifacts)...)
 		errs = append(errs, validatePortForwardResources(config, config.PortForward)...)
 		errs = append(errs, validateJibPluginTypes(config, config.Build.Artifacts)...)
+		errs = append(errs, validateKoSync(config, config.Build.Artifacts)...)
 		errs = append(errs, validateLogPrefix(config, config.Deploy.Logs)...)
 		errs = append(errs, validateArtifactTypes(config, config.Build)...)
 		errs = append(errs, validateTaggingPolicy(config, config.Build)...)
@@ -606,6 +607,31 @@ func validateJibPluginTypes(cfg *parser.SkaffoldConfigEntry, artifacts []*latest
 	return
 }
 
+// validateKoSync ensures that infer sync patterns contain the `kodata` string, since infer sync for the ko builder only supports static assets.
+func validateKoSync(cfg *parser.SkaffoldConfigEntry, artifacts []*latest.Artifact) []ErrorWithLocation {
+	var cfgErrs []ErrorWithLocation
+	for i, a := range artifacts {
+		if a.KoArtifact == nil || a.Sync == nil {
+			continue
+		}
+		if len(a.Sync.Infer) > 0 && strings.Contains(a.KoArtifact.Main, "...") {
+			cfgErrs = append(cfgErrs, ErrorWithLocation{
+				Error:    fmt.Errorf("artifact %s cannot use inferred file sync when the ko.main field contains the '...' wildcard. Instead, specify the path to the main package without using wildcards", a.ImageName),
+				Location: cfg.YAMLInfos.LocateField(cfg.Build.Artifacts[i].KoArtifact, "Main"),
+			})
+		}
+		for _, pattern := range a.Sync.Infer {
+			if !strings.Contains(pattern, "kodata") {
+				cfgErrs = append(cfgErrs, ErrorWithLocation{
+					Error:    fmt.Errorf("artifact %s has an invalid pattern %s for inferred file sync with the ko builder. The pattern must specify the 'kodata' directory. For instance, if you want to sync all static content, and your main package is in the workspace directory, you can use the pattern 'kodata/**/*'", a.ImageName, pattern),
+					Location: cfg.YAMLInfos.LocateField(cfg.Build.Artifacts[i].Sync, "Infer"),
+				})
+			}
+		}
+	}
+	return cfgErrs
+}
+
 // validateArtifactTypes checks that the artifact types are compatible with the specified builder.
 func validateArtifactTypes(cfg *parser.SkaffoldConfigEntry, bc latest.BuildConfig) []ErrorWithLocation {
 	cfgErrs := []ErrorWithLocation{}
diff --git a/pkg/skaffold/sync/sync.go b/pkg/skaffold/sync/sync.go
index a3cad27f3..9914b1a52 100644
--- a/pkg/skaffold/sync/sync.go
+++ b/pkg/skaffold/sync/sync.go
@@ -34,6 +34,7 @@ import (
 	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/build"
 	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/build/buildpacks"
 	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/build/jib"
+	kosync "github.com/GoogleContainerTools/skaffold/pkg/skaffold/build/ko/sync"
 	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/docker"
 	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/filemon"
 	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/graph"
@@ -106,6 +107,21 @@ func syncItem(ctx context.Context, a *latest.Artifact, tag string, e filemon.Eve
 }
 
 func inferredSyncItem(ctx context.Context, a *latest.Artifact, tag string, e filemon.Events, cfg docker.Config) (*Item, error) {
+	// the ko builder doesn't need or use a sync map
+	if a.KoArtifact != nil {
+		log.Entry(ctx).Debugf("ko inferred sync %+v", e)
+		toCopy, toDelete, err := kosync.Infer(ctx, a, e)
+		if err != nil {
+			return nil, err
+		}
+		return &Item{
+			Image:    tag,
+			Artifact: a,
+			Copy:     toCopy,
+			Delete:   toDelete,
+		}, nil
+	}
+
 	// deleted files are no longer contained in the syncMap, so we need to rebuild
 	if len(e.Deleted) > 0 {
 		return nil, nil


Test patch:
diff --git a/pkg/skaffold/build/ko/sync/sync_test.go b/pkg/skaffold/build/ko/sync/sync_test.go
new file mode 100644
index 000000000..12c946f69
--- /dev/null
+++ b/pkg/skaffold/build/ko/sync/sync_test.go
@@ -0,0 +1,176 @@
+/*
+Copyright 2022 The Skaffold Authors
+
+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 sync
+
+import (
+	"context"
+	"path/filepath"
+	"testing"
+
+	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/schema/latest"
+	"github.com/GoogleContainerTools/skaffold/testutil"
+)
+
+func TestInferSync(t *testing.T) {
+	tests := []struct {
+		description string
+		artifact    *latest.Artifact
+		files       []string
+		expected    map[string][]string
+	}{
+		{
+			description: "matching and non-matching files",
+			artifact: &latest.Artifact{
+				ArtifactType: latest.ArtifactType{
+					KoArtifact: &latest.KoArtifact{},
+				},
+				Sync: &latest.Sync{
+					Infer: []string{"kodata/**/*.js"},
+				},
+			},
+			files: []string{
+				"kodata/foo.js",
+				"kodata/bar.css",
+				"kodata/baz/frob.js",
+				"main.go",
+			},
+			expected: map[string][]string{
+				"kodata/foo.js":      {"/var/run/ko/foo.js"},
+				"kodata/baz/frob.js": {"/var/run/ko/baz/frob.js"},
+			},
+		},
+	}
+	for _, test := range tests {
+		testutil.Run(t, test.description, func(t *testutil.T) {
+			actual, err := inferSync(context.TODO(), test.artifact, test.files)
+			t.CheckNoError(err)
+			t.CheckDeepEqual(test.expected, actual)
+		})
+	}
+}
+
+func TestSyncDest(t *testing.T) {
+	tests := []struct {
+		description   string
+		file          string
+		workspace     string
+		localBasePath string
+		patterns      []string
+		expectedDest  string
+		wantErr       bool
+	}{
+		{
+			description:   "matching pattern simple",
+			file:          "kodata/page.html",
+			localBasePath: "kodata",
+			patterns:      []string{"kodata/page.html"},
+			expectedDest:  "/var/run/ko/page.html",
+		},
+		{
+			description:   "matching pattern doublestar",
+			file:          "kodata/page.html",
+			localBasePath: "kodata",
+			patterns:      []string{"kodata/**/*"},
+			expectedDest:  "/var/run/ko/page.html",
+		},
+		{
+			description:   "both non-matching and matching pattern",
+			file:          "kodata/page.html",
+			localBasePath: "kodata",
+			patterns:      []string{"kodata/*.css", "kodata/**/*"},
+			expectedDest:  "/var/run/ko/page.html",
+		},
+		{
+			description:   "no matching pattern",
+			file:          "kodata/page.html",
+			localBasePath: "kodata",
+			expectedDest:  "",
+		},
+		{
+			description:   "non-default directories",
+			file:          "workspace/cmd/foo/kodata/page.html",
+			workspace:     "workspace",
+			localBasePath: "workspace/cmd/foo/kodata",
+			patterns:      []string{"cmd/foo/kodata/**/*"},
+			expectedDest:  "/var/run/ko/page.html",
+		},
+		{
+			description:   "error on invalid pattern",
+			file:          "kodata/page.html",
+			localBasePath: "kodata",
+			patterns:      []string{"["},
+			wantErr:       true,
+		},
+	}
+	for _, test := range tests {
+		testutil.Run(t, test.description, func(t *testutil.T) {
+			actualDest, err := syncDest(test.file, test.workspace, test.localBasePath, test.patterns)
+			t.CheckError(test.wantErr, err)
+			t.CheckDeepEqual(test.expectedDest, actualDest)
+		})
+	}
+}
+
+func TestFindLocalKodataPath(t *testing.T) {
+	tests := []struct {
+		description  string
+		artifact     *latest.Artifact
+		expectedPath string
+		wantErr      bool
+	}{
+		{
+			description: "default",
+			artifact: &latest.Artifact{
+				ArtifactType: latest.ArtifactType{
+					KoArtifact: &latest.KoArtifact{},
+				},
+			},
+			expectedPath: "kodata",
+		},
+		{
+			description: "all values set",
+			artifact: &latest.Artifact{
+				ArtifactType: latest.ArtifactType{
+					KoArtifact: &latest.KoArtifact{
+						Dir:  "dir",
+						Main: "main",
+					},
+				},
+				Workspace: "workspace",
+			},
+			expectedPath: filepath.Join("workspace", "dir", "main", "kodata"),
+		},
+		{
+			description: "error due to main wildcard",
+			artifact: &latest.Artifact{
+				ArtifactType: latest.ArtifactType{
+					KoArtifact: &latest.KoArtifact{
+						Main: "./...",
+					},
+				},
+			},
+			wantErr: true,
+		},
+	}
+	for _, test := range tests {
+		testutil.Run(t, test.description, func(t *testutil.T) {
+			actualPath, err := findLocalKodataPath(test.artifact)
+			t.CheckError(test.wantErr, err)
+			t.CheckDeepEqual(test.expectedPath, actualPath)
+		})
+	}
+}
diff --git a/pkg/skaffold/docker/manifest_list_test.go b/pkg/skaffold/docker/manifest_list_test.go
new file mode 100644
index 000000000..3ad6b4941
--- /dev/null
+++ b/pkg/skaffold/docker/manifest_list_test.go
@@ -0,0 +1,74 @@
+/*
+Copyright 2022 The Skaffold Authors
+
+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 docker
+
+import (
+	"context"
+	"strings"
+	"testing"
+
+	"github.com/google/go-containerregistry/pkg/name"
+	v1 "github.com/google/go-containerregistry/pkg/v1"
+	"github.com/google/go-containerregistry/pkg/v1/mutate"
+	"github.com/google/go-containerregistry/pkg/v1/remote"
+
+	"github.com/GoogleContainerTools/skaffold/testutil"
+)
+
+func (i fakeImage) Descriptor() (*v1.Descriptor, error) {
+	return &v1.Descriptor{}, nil
+}
+
+func TestCreateManifestList(t *testing.T) {
+	ctx := context.Background()
+	targetTag := "gcr.io/skaffold/example:latest"
+	images := []SinglePlatformImage{
+		{Platform: &v1.Platform{OS: "linux", Architecture: "amd64"}, Image: "gcr.io/skaffold/example:b1234_linux_amd64"},
+		{Platform: &v1.Platform{OS: "linux", Architecture: "arm64"}, Image: "gcr.io/skaffold/example:b1234_linux_arm64"},
+	}
+
+	testutil.Run(t, "", func(t *testutil.T) {
+		t.Override(&remoteImage, func(ref name.Reference, options ...remote.Option) (v1.Image, error) {
+			return &fakeImage{
+				Reference: ref,
+			}, nil
+		})
+
+		t.Override(&mutateAppendManifest, func(base v1.ImageIndex, adds ...mutate.IndexAddendum) v1.ImageIndex {
+			for i, add := range adds {
+				img := add.Add.(*fakeImage).Reference.Name()
+				t.CheckDeepEqual(images[i].Image, img)
+			}
+
+			return &fakeImageIndex{}
+		})
+
+		t.Override(&remoteWriteIndex, func(ref name.Reference, ii v1.ImageIndex, options ...remote.Option) (rerr error) {
+			return nil
+		})
+
+		manifestTag, err := CreateManifestList(ctx, images, targetTag)
+
+		if err != nil {
+			t.Fatalf("Error generating manifest list with target tag %s\n", targetTag)
+		}
+
+		if !strings.HasPrefix(manifestTag, targetTag) {
+			t.Fatalf("Error in tag %s, %s not found\n", manifestTag, targetTag)
+		}
+	})
+}
diff --git a/pkg/skaffold/platform/resolver_test.go b/pkg/skaffold/platform/resolver_test.go
index 7bb06e527..04db385a8 100644
--- a/pkg/skaffold/platform/resolver_test.go
+++ b/pkg/skaffold/platform/resolver_test.go
@@ -27,7 +27,6 @@ import (
 	"k8s.io/client-go/kubernetes"
 	fakeclient "k8s.io/client-go/kubernetes/fake"
 
-	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/config"
 	kubernetesclient "github.com/GoogleContainerTools/skaffold/pkg/skaffold/kubernetes/client"
 	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/schema/latest"
 	"github.com/GoogleContainerTools/skaffold/testutil"
@@ -35,22 +34,22 @@ import (
 
 func TestResolver(t *testing.T) {
 	tests := []struct {
-		description      string
-		cliPlatforms     []string
-		clusterPlatforms []string
-		pipelines        []latest.Pipeline
-		runMode          config.RunMode
-		shouldErr        bool
-		expected         map[string]Matcher
+		description           string
+		cliPlatforms          []string
+		clusterPlatforms      []string
+		pipelines             []latest.Pipeline
+		checkClusterPlatforms bool
+		disableMultiPlatform  bool
+		shouldErr             bool
+		expected              map[string]Matcher
 	}{
 		{
-			description:      "all platforms specified valid for `build` mode",
+			description:      "all platforms specified valid; multiplat enabled",
 			cliPlatforms:     []string{"linux/amd64", "linux/386"},
 			clusterPlatforms: []string{"linux/arm64"},
 			pipelines: []latest.Pipeline{{Build: latest.BuildConfig{
 				Platforms: []string{"windows/amd64"},
 				Artifacts: []*latest.Artifact{{ImageName: "img1"}, {ImageName: "img2"}}}}},
-			runMode: config.RunModes.Build,
 			expected: map[string]Matcher{
 				"img1": {Platforms: []v1.Platform{
 					{OS: "linux", Architecture: "amd64"}, {OS: "linux", Architecture: "386"},
@@ -61,39 +60,28 @@ func TestResolver(t *testing.T) {
 			},
 		},
 		{
-			description:      "cluster platform mismatch for `dev` mode",
+			description:      "cluster platform mismatch; multiplat disabled",
 			cliPlatforms:     []string{"linux/amd64", "linux/386"},
 			clusterPlatforms: []string{"linux/arm64"},
 			pipelines: []latest.Pipeline{{Build: latest.BuildConfig{
 				Platforms: []string{"windows/amd64"},
 				Artifacts: []*latest.Artifact{{ImageName: "img1"}, {ImageName: "img2"}}}}},
-			runMode: config.RunModes.Dev,
-			expected: map[string]Matcher{
-				"img1": {Platforms: []v1.Platform{{OS: "linux", Architecture: "amd64"}, {OS: "linux", Architecture: "386"}}},
-				"img2": {Platforms: []v1.Platform{{OS: "linux", Architecture: "amd64"}, {OS: "linux", Architecture: "386"}}},
-			},
-		},
-		{
-			description:      "cluster platform selected for `dev` mode",
-			cliPlatforms:     []string{"linux/amd64", "linux/386"},
-			clusterPlatforms: []string{"linux/amd64"},
-			pipelines: []latest.Pipeline{{Build: latest.BuildConfig{
-				Platforms: []string{"windows/amd64"},
-				Artifacts: []*latest.Artifact{{ImageName: "img1"}, {ImageName: "img2"}}}}},
-			runMode: config.RunModes.Dev,
+			checkClusterPlatforms: true,
+			disableMultiPlatform:  true,
 			expected: map[string]Matcher{
 				"img1": {Platforms: []v1.Platform{{OS: "linux", Architecture: "amd64"}}},
 				"img2": {Platforms: []v1.Platform{{OS: "linux", Architecture: "amd64"}}},
 			},
 		},
 		{
-			description:      "cluster platform selected for `debug` mode",
+			description:      "cluster platform selected; multiplat disabled",
 			cliPlatforms:     []string{"linux/amd64", "linux/386"},
 			clusterPlatforms: []string{"linux/amd64"},
 			pipelines: []latest.Pipeline{{Build: latest.BuildConfig{
 				Platforms: []string{"windows/amd64"},
 				Artifacts: []*latest.Artifact{{ImageName: "img1"}, {ImageName: "img2"}}}}},
-			runMode: config.RunModes.Debug,
+			checkClusterPlatforms: true,
+			disableMultiPlatform:  true,
 			expected: map[string]Matcher{
 				"img1": {Platforms: []v1.Platform{{OS: "linux", Architecture: "amd64"}}},
 				"img2": {Platforms: []v1.Platform{{OS: "linux", Architecture: "amd64"}}},
@@ -117,18 +105,26 @@ func TestResolver(t *testing.T) {
 			pipelines: []latest.Pipeline{{Build: latest.BuildConfig{
 				Platforms: []string{"windows/amd64"},
 				Artifacts: []*latest.Artifact{{ImageName: "img1", Platforms: []string{"darwin/arm64"}}, {ImageName: "img2"}}}}},
-			runMode:   config.RunModes.Dev,
-			shouldErr: true,
+			checkClusterPlatforms: true,
+			disableMultiPlatform:  true,
+			shouldErr:             true,
 		},
 	}
 
 	for _, test := range tests {
+		host := Matcher{Platforms: []v1.Platform{{Architecture: "amd64", OS: "linux"}}}
 		testutil.Run(t, test.description, func(t *testutil.T) {
-			t.Override(&getClusterPlatforms, func(context.Context, string, bool) (Matcher, error) {
+			t.Override(&getClusterPlatforms, func(context.Context, string) (Matcher, error) {
 				return Parse(test.clusterPlatforms)
 			})
+			t.Override(&getHostMatcher, func() Matcher { return host })
 
-			r, err := NewResolver(context.Background(), test.pipelines, test.cliPlatforms, test.runMode, "")
+			opts := ResolverOpts{
+				CliPlatformsSelection:     test.cliPlatforms,
+				DisableMultiPlatformBuild: test.disableMultiPlatform,
+				CheckClusterNodePlatforms: test.checkClusterPlatforms,
+			}
+			r, err := NewResolver(context.Background(), test.pipelines, opts)
 			t.CheckError(test.shouldErr, err)
 			if !test.shouldErr {
 				t.CheckMapsMatch(test.expected, r.platformsByImageName)
@@ -137,6 +133,51 @@ func TestResolver(t *testing.T) {
 	}
 }
 
+func TestSelectOnePlatform(t *testing.T) {
+	host := Matcher{Platforms: []v1.Platform{{Architecture: "amd64", OS: "linux"}}}
+	tests := []struct {
+		description string
+		input       Matcher
+		expected    Matcher
+	}{
+		{
+			description: "empty",
+			input:       Matcher{},
+			expected:    Matcher{},
+		},
+		{
+			description: "all matcher",
+			input:       Matcher{All: true},
+			expected:    host,
+		},
+		{
+			description: "matching host",
+			input: Matcher{Platforms: []v1.Platform{
+				{Architecture: "arm", OS: "freebsd"},
+				{Architecture: "amd64", OS: "linux"},
+			}},
+			expected: host,
+		},
+		{
+			description: "not matching host",
+			input: Matcher{Platforms: []v1.Platform{
+				{Architecture: "arm", OS: "freebsd"},
+				{Architecture: "arm", OS: "linux"},
+			}},
+			expected: Matcher{Platforms: []v1.Platform{
+				{Architecture: "arm", OS: "freebsd"},
+			}},
+		},
+	}
+	for _, test := range tests {
+		testutil.Run(t, test.description, func(t *testutil.T) {
+			t.Override(&getHostMatcher, func() Matcher { return host })
+			actual := selectOnePlatform(test.input)
+			t.CheckDeepEqual(test.expected, actual)
+		})
+	}
+}
+
 func TestGetClusterPlatforms(t *testing.T) {
 	tests := []struct {
 		description  string
@@ -146,19 +187,7 @@ func TestGetClusterPlatforms(t *testing.T) {
 		isDevOrDebug bool
 	}{
 		{
-			description: "homogeneous node platforms; not dev or debug",
-			nodes: []node{
-				{operatingSystem: "linux", architecture: "amd64"},
-				{operatingSystem: "linux", architecture: "amd64"},
-				{operatingSystem: "linux", architecture: "amd64"},
-			},
-			host: Matcher{Platforms: []v1.Platform{{OS: "linux", Architecture: "386"}}},
-			expected: Matcher{Platforms: []v1.Platform{
-				{OS: "linux", Architecture: "amd64"},
-			}},
-		},
-		{
-			description: "homogeneous node platforms; is dev or debug",
+			description: "homogeneous node platforms",
 			nodes: []node{
 				{operatingSystem: "linux", architecture: "amd64"},
 				{operatingSystem: "linux", architecture: "amd64"},
@@ -168,10 +197,9 @@ func TestGetClusterPlatforms(t *testing.T) {
 			expected: Matcher{Platforms: []v1.Platform{
 				{OS: "linux", Architecture: "amd64"},
 			}},
-			isDevOrDebug: true,
 		},
 		{
-			description: "heterogeneous node platforms; not dev or debug",
+			description: "heterogeneous node platforms",
 			nodes: []node{
 				{operatingSystem: "linux", architecture: "amd64"},
 				{operatingSystem: "linux", architecture: "arm64"},
@@ -182,32 +210,6 @@ func TestGetClusterPlatforms(t *testing.T) {
 				{OS: "linux", Architecture: "amd64"}, {OS: "linux", Architecture: "arm64"},
 			}},
 		},
-		{
-			description: "heterogeneous node platforms; is dev or debug; matching host",
-			nodes: []node{
-				{operatingSystem: "linux", architecture: "amd64"},
-				{operatingSystem: "linux", architecture: "arm64"},
-				{operatingSystem: "linux", architecture: "amd64"},
-			},
-			host: Matcher{Platforms: []v1.Platform{{OS: "linux", Architecture: "arm64"}}},
-			expected: Matcher{Platforms: []v1.Platform{
-				{OS: "linux", Architecture: "arm64"},
-			}},
-			isDevOrDebug: true,
-		},
-		{
-			description: "heterogeneous node platforms; is dev or debug; not matching host",
-			nodes: []node{
-				{operatingSystem: "linux", architecture: "amd64"},
-				{operatingSystem: "linux", architecture: "arm64"},
-				{operatingSystem: "linux", architecture: "amd64"},
-			},
-			host: Matcher{Platforms: []v1.Platform{{OS: "windows", Architecture: "amd64"}}},
-			expected: Matcher{Platforms: []v1.Platform{
-				{OS: "linux", Architecture: "amd64"},
-			}},
-			isDevOrDebug: true,
-		},
 	}
 	for _, test := range tests {
 		testutil.Run(t, test.description, func(t *testutil.T) {
@@ -215,7 +217,7 @@ func TestGetClusterPlatforms(t *testing.T) {
 				return fakeKubernetesClient(test.nodes)
 			})
 			t.Override(&getHostMatcher, func() Matcher { return test.host })
-			m, err := GetClusterPlatforms(context.Background(), "", test.isDevOrDebug)
+			m, err := GetClusterPlatforms(context.Background(), "")
 			t.CheckErrorAndDeepEqual(false, err, test.expected, m)
 		})
 	}
diff --git a/pkg/skaffold/schema/validation/validation_test.go b/pkg/skaffold/schema/validation/validation_test.go
index 9fc1b20a9..3fde31fde 100644
--- a/pkg/skaffold/schema/validation/validation_test.go
+++ b/pkg/skaffold/schema/validation/validation_test.go
@@ -1155,6 +1155,82 @@ func TestValidateJibPluginType(t *testing.T) {
 	}
 }
 
+func TestValidateKoSync(t *testing.T) {
+	tests := []struct {
+		description string
+		artifacts   []*latest.Artifact
+		wantErr     bool
+	}{
+		{
+			description: "basic infer sync with no errors",
+			artifacts: []*latest.Artifact{{
+				ArtifactType: latest.ArtifactType{
+					KoArtifact: &latest.KoArtifact{},
+				},
+				ImageName: "test",
+				Sync: &latest.Sync{
+					Infer: []string{"kodata/**/*"},
+				},
+			}},
+		},
+		{
+			description: "no error for wildcard in Main when no infer sync set up",
+			artifacts: []*latest.Artifact{{
+				ArtifactType: latest.ArtifactType{
+					KoArtifact: &latest.KoArtifact{
+						Main: "./...",
+					},
+				},
+				ImageName: "test",
+			}},
+		},
+		{
+			description: "error for wildcard in Main when infer sync is set up",
+			artifacts: []*latest.Artifact{{
+				ArtifactType: latest.ArtifactType{
+					KoArtifact: &latest.KoArtifact{
+						Main: "./...",
+					},
+				},
+				Sync: &latest.Sync{
+					Infer: []string{"kodata/**/*"},
+				},
+			}},
+			wantErr: true,
+		},
+		{
+			description: "error for patterns outside kodata",
+			artifacts: []*latest.Artifact{{
+				ArtifactType: latest.ArtifactType{
+					KoArtifact: &latest.KoArtifact{},
+				},
+				Sync: &latest.Sync{
+					Infer: []string{"**/*"},
+				},
+			}},
+			wantErr: true,
+		},
+	}
+	for _, test := range tests {
+		testutil.Run(t, test.description, func(t *testutil.T) {
+			cfg := &parser.SkaffoldConfigEntry{
+				SkaffoldConfig: &latest.SkaffoldConfig{
+					APIVersion: latest.Version,
+					Kind:       "Config",
+					Pipeline: latest.Pipeline{
+						Build: latest.BuildConfig{
+							Artifacts: test.artifacts,
+						},
+					},
+				},
+				YAMLInfos: configlocations.NewYAMLInfos(),
+			}
+			err := Process(parser.SkaffoldConfigSet{cfg}, Options{CheckDeploySource: false})
+			t.CheckError(test.wantErr, err)
+		})
+	}
+}
+
 func TestValidateLogsConfig(t *testing.T) {
 	tests := []struct {
 		prefix    string
@@ -1333,9 +1409,11 @@ func TestValidateAcyclicDependencies(t *testing.T) {
 // setDependencies constructs a graph of artifact dependencies using the map as an adjacency list representation of indices in the artifacts array.
 // For example:
 // m = {
-//    0 : {1, 2},
-//    2 : {3},
-//}
+//
+//	0 : {1, 2},
+//	2 : {3},
+//
+// }
 // implies that a[0] artifact depends on a[1] and a[2]; and a[2] depends on a[3].
 func setDependencies(a []*latest.Artifact, d map[int][]int) {
 	for k, dep := range d {
diff --git a/pkg/skaffold/sync/sync_test.go b/pkg/skaffold/sync/sync_test.go
index bde29a40d..5472559dd 100644
--- a/pkg/skaffold/sync/sync_test.go
+++ b/pkg/skaffold/sync/sync_test.go
@@ -13,6 +13,7 @@ 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 sync
 
 import (
@@ -747,6 +748,42 @@ func TestNewSyncItem(t *testing.T) {
 				Delete: nil,
 			},
 		},
+
+		// Infer with Ko
+		{
+			description: "infer: ko static assets",
+			artifact: &latest.Artifact{
+				ArtifactType: latest.ArtifactType{
+					KoArtifact: &latest.KoArtifact{},
+				},
+				ImageName: "test",
+				Sync: &latest.Sync{
+					Infer: []string{"kodata/**/*"},
+				},
+			},
+			builds: []graph.Artifact{{
+				ImageName: "test",
+				Tag:       "test:123",
+			}},
+			evt: filemon.Events{
+				Added: []string{filepath.Join("kodata", "foo", "bar.html")},
+				Modified: []string{
+					filepath.Join("kodata", "frob", "baz.js"),
+					"main.go",
+				},
+				Deleted: []string{filepath.Join("kodata", "corge", "grault.css")},
+			},
+			expected: &Item{
+				Image: "test:123",
+				Copy: map[string][]string{
+					filepath.Join("kodata", "foo", "bar.html"): {"/var/run/ko/foo/bar.html"},
+					filepath.Join("kodata", "frob", "baz.js"):  {"/var/run/ko/frob/baz.js"},
+				},
+				Delete: map[string][]string{
+					filepath.Join("kodata", "corge", "grault.css"): {"/var/run/ko/corge/grault.css"},
+				},
+			},
+		},
 	}
 	for _, test := range tests {
 		testutil.Run(t, test.description, func(t *testutil.T) {