Repository: docker/compose
File: pkg/compose/build.go (Go)

Code before:
					}
				}
				return nil
			},
		)(ctx)
		if err != nil {
			return err
		}
	}

	// set digest as com.docker.compose.image label so we can detect outdated containers
	for name, service := range project.Services {
		image := api.GetImageNameOrDefault(service, project.Name)
		img, ok := images[image]
		if ok {
			service.CustomLabels.Add(api.ImageDigestLabel, img.ID)
		}

		resolveImageVolumes(service, images, project.Name)

		project.Services[name] = service
	}
	return nil
}

func resolveImageVolumes(service types.ServiceConfig, images map[string]api.ImageSummary, projectName string) {
	for i, vol := range service.Volumes {
		if vol.Type == types.VolumeTypeImage {
			imgName := vol.Source
			if _, ok := images[vol.Source]; !ok {
				// check if source is another service in the project
				imgName = api.GetImageNameOrDefault(types.ServiceConfig{Name: vol.Source}, projectName)
				// If we still can't find it, it might be an external image that wasn't pulled yet or doesn't exist
				if _, ok := images[imgName]; !ok {
					continue
				}
			}
			if img, ok := images[imgName]; ok {
				ref, err := reference.ParseDockerRef(vol.Source)
				if err != nil {
					logrus.Warnf("failed to parse image reference %q: %v", vol.Source, err)
					continue
				}
				digested, err := reference.WithDigest(reference.TrimNamed(ref), digest.Digest(img.ID))
				if err != nil {
					logrus.Warnf("failed to add digest to image reference %q: %v", vol.Source, err)
					continue
				}
				service.Volumes[i].Source = digested.String()
			}
		}

Review comment (suggestion):
I wonder this works as expected as you don't pass a `*types.ServiceConfig` so struct is copied in function scope, leaving original unmodified.

Code after:
				return nil
			},
		)(ctx)
		if err != nil {
			return err
		}
	}

	// set digest as com.docker.compose.image label so we can detect outdated containers
	for name, service := range project.Services {
		image := api.GetImageNameOrDefault(service, project.Name)
		img, ok := images[image]
		if ok {
			service.CustomLabels.Add(api.ImageDigestLabel, img.ID)
		}

		resolveImageVolumes(&service, images, project.Name)

		project.Services[name] = service
	}
	return nil
}

func resolveImageVolumes(service *types.ServiceConfig, images map[string]api.ImageSummary, projectName string) {
	for i, vol := range service.Volumes {
		if vol.Type == types.VolumeTypeImage {
			imgName := vol.Source
			if _, ok := images[vol.Source]; !ok {
				// check if source is another service in the project
				imgName = api.GetImageNameOrDefault(types.ServiceConfig{Name: vol.Source}, projectName)
				// If we still can't find it, it might be an external image that wasn't pulled yet or doesn't exist
				if _, ok := images[imgName]; !ok {
					continue
				}
			}
			if img, ok := images[imgName]; ok {
				// Use Image ID directly as source.
				// Using name@digest format (via reference.WithDigest) fails for local-only images
				// that don't have RepoDigests (e.g. built locally in CI).
				// Image ID (sha256:...) is always valid and ensures ServiceHash changes on rebuild.
				service.Volumes[i].Source = img.ID
			}
		}
	}
}

func (s *composeService) getLocalImagesDigests(ctx context.Context, project *types.Project) (map[string]api.ImageSummary, error) {
	imageNames := utils.Set[string]{}
	for _, s := range project.Services {
		imageNames.Add(api.GetImageNameOrDefault(s, project.Name))
		for _, volume := range s.Volumes {