3.8 KiB · text History b868907
defmodule GitGud.Packages.BuildCacheTest do
  @moduledoc """
  A BuildKit registry cache (`--cache-to type=registry,ref=…:buildcache`)
  is pushed after the real tags, and must not become a container
  package's `latest_version` (`PackageVersion.build_cache?/1`).
  """
  use GitGud.DataCase, async: true

  import GitGud.ForgeFixtures

  alias GitGud.Packages
  alias GitGud.Packages.PackageVersion

  @cache "application/vnd.buildkit.cacheconfig.v0"

  describe "build_cache?/1" do
    test "an index listing BuildKit's cache config" do
      doc = %{
        "mediaType" => "application/vnd.oci.image.index.v1+json",
        "manifests" => [
          %{"mediaType" => "application/vnd.oci.image.layer.v1.tar+gzip"},
          %{"mediaType" => @cache}
        ]
      }

      assert PackageVersion.build_cache?(%{version: "whatever", manifest_doc: doc})
    end

    test "an image whose config is BuildKit's cache config" do
      doc = %{"config" => %{"mediaType" => @cache}, "layers" => []}
      assert PackageVersion.build_cache?(%{version: "x", manifest_doc: doc})
    end

    test "a real image isn't one, whatever its tag is called" do
      doc = %{"config" => %{"mediaType" => "application/vnd.oci.image.config.v1+json"}}
      refute PackageVersion.build_cache?(%{version: "cache", manifest_doc: doc})
    end

    test "with no manifest, the tag's name decides" do
      for tag <- ~w(buildcache cli-buildcache cache build-cache app_cache) do
        assert PackageVersion.build_cache?(%{version: tag, manifest_doc: %{}}), tag
      end

      for tag <- ~w(latest master v1.2.0 sha-cf87d70 cachet precache) do
        refute PackageVersion.build_cache?(%{version: tag, manifest_doc: nil}), tag
      end
    end
  end

  describe "latest_version" do
    setup do
      {_user, repo} = repository_fixture()
      {:ok, repo: repo}
    end

    defp push(repo, tag, at) do
      {:ok, {package, _}} =
        Packages.record_push(repo, "container", "app", %{
          version: tag,
          digest: "sha256:" <> String.duplicate("a", 64),
          pushed_at: at
        })

      Repo.reload!(package)
    end

    test "a cache pushed after the real tags doesn't take it", %{repo: repo} do
      push(repo, "v1.2.0", ~U[2026-11-05 16:09:41Z])
      push(repo, "latest", ~U[2026-11-05 16:09:42Z])
      package = push(repo, "buildcache", ~U[2026-11-05 16:10:09Z])

      assert package.latest_version == "latest"
    end

    test "a cache is the latest when it's all there is", %{repo: repo} do
      assert push(repo, "buildcache", ~U[2026-11-05 16:10:09Z]).latest_version == "buildcache"
      assert push(repo, "v1", ~U[2026-11-05 16:11:00Z]).latest_version == "v1"
    end

    test "deleting the latest falls back past caches", %{repo: repo} do
      push(repo, "v1", ~U[2026-11-05 16:00:00Z])
      push(repo, "buildcache", ~U[2026-11-05 16:05:00Z])
      push(repo, "v2", ~U[2026-11-05 16:10:00Z])

      {:ok, :updated} = Packages.delete_version(repo, "container", "app", "v2")

      assert Packages.get_package_by(repo.id, "container", "app").latest_version == "v1"
    end

    test "the migration repairs a package stuck on a cache", %{repo: repo} do
      push(repo, "v1", ~U[2026-11-05 16:00:00Z])
      push(repo, "latest", ~U[2026-11-05 16:09:41Z])
      push(repo, "buildcache", ~U[2026-11-05 16:10:09Z])

      # As the old rule left it: the newest push, the cache.
      Packages.get_package_by(repo.id, "container", "app")
      |> Ecto.Changeset.change(latest_version: "buildcache")
      |> Repo.update!()

      Code.require_file(
        "priv/repo/migrations/20261110011023_skip_build_caches_as_latest_package_version.exs"
      )

      Repo.query!(GitGud.Repo.Migrations.SkipBuildCachesAsLatestPackageVersion.sql())

      assert Packages.get_package_by(repo.id, "container", "app").latest_version == "latest"
    end
  end
end